【问题标题】:How different async programming is from Threads?异步编程与线程有什么不同?
【发布时间】:2012-06-13 05:31:38
【问题描述】:

我在这里阅读了一些async 的文章:http://www.asp.net/web-forms/tutorials/aspnet-45/using-asynchronous-methods-in-aspnet-45,作者说:

当您进行异步工作时,您并不总是使用线程。 例如,当您发出异步 Web 服务请求时, ASP.NET 不会在异步方法调用之间使用任何线程 和等待。

所以我想了解的是,如果我们不使用任何线程进行并发执行,它如何变成async? “你并不总是使用线程”是什么意思?

让我先解释一下我对使用线程的了解(一个简单的例子,当然线程可以在除了 UI 和 Worker 方法之外的不同情况下使用)

  1. 您有 UI 线程来接受输入,提供输出。
  2. 您可以在 UI 线程中处理事情,但它会使 UI 无响应。
  3. 假设我们有一个与流相关的操作,我们需要下载某种数据。
  4. 我们还允许用户在下载时执行其他操作。
  5. 我们创建一个新的工作线程来下载文件并更改进度条。
  6. 一旦完成,没有什么可做的,所以线程被杀死。
  7. 我们从 UI 线程继续。

我们可以根据情况在 UI 线程中等待工作线程,但在下载文件之前,我们可以通过 UI 线程做其他事情,然后等待工作线程。

async 编程不一样吗?如果不是,有什么区别?我读到async 编程使用ThreadPool 来拉线程。

【问题讨论】:

    标签: c# asp.net .net asynchronous


    【解决方案1】:

    异步编程不需要线程。

    “异步”表示 API 不会阻塞 调用 线程。它确实意味着有另一个线程正在阻塞。

    首先,考虑您的 UI 示例,这次使用实际的异步 API:

    1. 您有 UI 线程来接受输入,提供输出。
    2. 您可以在 UI 线程中处理事情,但这会使 UI 无响应。
    3. 假设我们有一个与流相关的操作,我们需要下载某种数据。
    4. 我们还允许用户在下载时执行其他操作。
    5. 我们使用异步 API 下载文件。不需要工作线程。
    6. 异步操作将其进度报告给 UI 线程(更新进度条),它还将其完成报告给 UI 线程(可以像任何其他事件一样对其进行响应)。

    这显示了如何只涉及一个线程(UI 线程),但同时也有异步操作。您可以启动多个异步操作,但只有一个线程参与这些操作 - 没有线程被阻塞。

    async/await 提供了一种非常好的语法,用于启动异步操作然后返回,并在该操作完成后让方法的其余部分继续。

    ASP.NET 与此类似,只是它没有主/UI 线程。相反,它对每个不完整的请求都有一个“请求上下文”。 ASP.NET 线程来自一个线程池,它们在处理请求时进入“请求上下文”;完成后,它们会退出“请求上下文”并返回线程池。

    ASP.NET 会跟踪每个请求的不完整异步操作,因此当线程返回线程池时,它会检查该请求是否正在进行任何异步操作;如果没有,则请求完成。

    因此,当您await ASP.NET 中的异步操作不完整时,线程将递增该计数器并返回。 ASP.NET 知道请求未完成,因为计数器不为零,因此它不会完成响应。线程返回线程池,此时:没有个线程在处理该请求。

    当异步操作完成时,它将async 方法的剩余部分调度到请求上下文中。 ASP.NET 抓取它的一个处理程序线程(它可能与执行async 方法的早期部分的线程相同,也可能不同),计数器递减,线程执行async 方法。

    ASP.NET vNext 略有不同;整个框架对异步处理程序有更多支持。但大体概念是一样的。

    更多信息:

    【讨论】:

    • +1 对“异步”的出色描述。就像“电压”一样,您需要两个参考点。
    • 当 ASP.NET 请求异步等待异步操作时,没有使用线程(因此,异步编程不需要线程)。当方法继续执行时,当然必须使用某个线程来执行代码。异步部分不需要线程。
    • 你如何实现这个““异步”意味着 API 不会阻塞调用线程。”如果你使用相同的线程?这是我不太明白的事情。除了主路径之外,您不需要其他执行路径来异步运行吗?
    • 一个线程执行代码。异步操作是您开始之后将完成的操作。如果操作不执行任何操作(例如,I/O 操作),则不需要线程。
    • @TheMuffinMan:破坏特定线程只能发生在 UI 应用程序中,而不是 ASP.NET。它通常通过使异步工作更“笨拙”而不是“健谈”来解决 - 一般准则是no more than 100 continuations per second。请记住 using ConfigureAwait(false) is a best practice,这实际上是每秒 100 次 UI 更新 - 远远超过人类的处理能力。
    【解决方案2】:

    当我第一次看到 asyncawait 时,我认为它们是异步编程模型的 C# 语法糖。我错了,asyncawait 不止于此。这是一个全新的异步模式基于任务的异步模式,http://www.microsoft.com/en-us/download/details.aspx?id=19957 是一篇很好的入门文章。大多数实现 TAP 的 FCL 类都是调用 APM 方法(BegingXXX() 和 EndXXX())。以下是 TAP 和 AMP 的两个代码快照:

    TAP 示例:

        static void Main(string[] args)
        {
            GetResponse();
            Console.ReadLine();
        }
    
        private static async Task<WebResponse> GetResponse()
        {
            var webRequest = WebRequest.Create("http://www.google.com");
            Task<WebResponse> response = webRequest.GetResponseAsync();
            Console.WriteLine(new StreamReader(response.Result.GetResponseStream()).ReadToEnd());
            return response.Result;
        }
    

    APM 示例:

        static void Main(string[] args)
        {
            var webRequest = WebRequest.Create("http://www.google.com");
            webRequest.BeginGetResponse(EndResponse, webRequest);
            Console.ReadLine();
        }
    
        static void EndResponse(IAsyncResult result)
        {
            var webRequest = (WebRequest) result.AsyncState;
            var response = webRequest.EndGetResponse(result);
            Console.WriteLine(new StreamReader(response.GetResponseStream()).ReadToEnd());
        }
    

    最后这两个会是一样的,因为GetResponseAsync()里面调用了BeginGetResponse()和EndGetResponse()。当我们反射GetResponseAsync()的源代码时,我们会得到这样的代码:

    task = Task<WebResponse>.Factory.FromAsync(
           new Func<AsyncCallback, object, IAsyncResult>(this.BeginGetResponse), 
           new Func<IAsyncResult, WebResponse>(this.EndGetResponse), null);
    

    对于 APM,在 BeginXXX() 中,有一个回调方法的参数,该方法将在任务(通常是 IO 繁重的操作)完成时调用。创建一个新线程并且是异步的,它们都会立即在主线程中返回,它们都是未阻塞的。在性能方面,在处理 I/O 绑定操作(例如读取文件、数据库操作和网络读取)时,创建新线程将消耗更多资源。创建新线程有两个缺点,

    1. 就像在你提到的文章中一样,有内存成本和 CLR
      线程池的限制。
    2. 将发生上下文切换。另一方面,异步将 不手动创建任何线程,它不会有上下文切换 当 IO 绑定操作返回时。

    这是一张有助于理解差异的图片:

    此图来自 MSDN 文章“Asynchronous Pages in ASP.NET 2.0”,它非常详细地解释了旧的异步在 ASP.NET 2.0 中如何工作。

    关于异步编程模型,请参阅 Jeffrey Richter 的文章“Implementing the CLR Asynchronous Programming Model”了解更多详情,也可参阅他的书“CLR via Csharp 3rd Edition”第 27 章。

    【讨论】:

    • 您能否详细说明一下“异步不会手动创建任何线程,并且在 IO 绑定操作返回时不会进行上下文切换。”请再多一点?
    • @Tarik,实际上它的线程池不是异步的,因为大多数 CLR APM 实现都使用线程池。对于上下文切换,当我们使用专用线程进行同步 IO-bound 操作时,在 IO 操作期间线程将阻塞直到完成,如果操作长于“时间片”,则会发生上下文切换。在异步中,请仔细看图片右侧,第一个线程调用BeginXxx(),全部完成,返回线程池,IO操作完成后,在第二个线程上继续。跨度>
    【解决方案3】:

    假设您正在实现一个 Web 应用程序,并且随着每个客户端请求进入 您的服务器,您需要发出数据库请求。当客户端请求进来时,一个线程池 线程将调用您的代码。如果现在同步发出数据库请求,线程将阻塞 无限期地等待数据库响应结果。如果在这段时间 另一个客户端请求进来,线程池将不得不创建另一个线程,然后再次 线程在发出另一个数据库请求时将阻塞。随着越来越多的客户请求进来, 创建的线程越来越多,所有这些线程都阻塞等待数据库响应。 结果是您的 Web 服务器分配了大量的系统资源(线程及其内存) 几乎没有使用! 更糟糕的是,当数据库确实回复各种结果时,线程变得 畅通无阻,它们都开始执行。但是因为你可能有很多线程正在运行并且相对 CPU 内核少,Windows 必须执行频繁的上下文切换,这甚至会损害性能 更多的。这不是实现可扩展应用程序的方法。

    为了从文件中读取数据,我现在调用 ReadAsync 而不是 Read。 ReadAsync 在内部分配一个 任务对象,表示读取操作的等待完成。然后,ReadAsync 调用 Win32 的 ReadFile 函数 (#1)。 ReadFile 分配它的 IRP,像在 同步场景(#2),然后将其传递给 Windows 内核(#3)。 Windows 添加 IRP 到硬盘驱动程序的 IRP 队列(#4),但是现在,不是阻塞你的线程,你的线程是 允许返回您的代码;您的线程会立即从对 ReadAsync 的调用中返回(#5、#6、 和#7)。现在,当然,IRP 还不一定被处理,所以你不能有代码 ReadAsync 尝试访问传入的 Byte[] 中的字节。

    【讨论】:

      猜你喜欢
      • 2012-11-05
      • 2016-04-13
      • 2011-11-13
      • 2010-10-24
      • 1970-01-01
      • 2013-04-26
      • 1970-01-01
      相关资源
      最近更新 更多