异步编程与线程有何不同?

33

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

  

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

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

让我先解释一下我对处理线程的了解(这里只是一个快速的例子,当然线程可以用于其他不同的情况):

  1. 您有UI线程来接收输入和输出。
  2. 您可以在UI线程中处理事务,但这会使UI无响应。
  3. 所以我们假设我们有一个与流相关的操作,并且我们需要下载某些数据。
  4. 我们还允许用户在下载期间执行其他操作。
  5. 我们创建新的工作线程来下载文件并更改进度条。
  6. 完成后,线程被终止。
  7. 我们从UI线程继续。

我们可以在UI线程中等待工作线程,具体取决于情况,但在此之前,在文件正在下载时,我们可以使用UI线程执行其他操作,然后等待工作线程。

async编程不是这样的吗?如果不是,有什么区别呢?我读到async编程使用ThreadPool来提取线程。

3个回答

35

线程在异步编程中并不是必需的。

“异步”意味着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跟踪每个请求的未完成异步操作,因此当线程返回到线程池时,它会检查该请求中是否有任何正在进行的异步操作;如果没有,则该请求已完成。
因此,当您在ASP.NET中使用await等待未完成的异步操作时,线程将增加计数器并返回。由于计数器是非零的,所以ASP.NET知道请求尚未完成,因此不会完成响应。线程返回到线程池,在这一点上:没有线程正在处理该请求。
当异步操作完成时,它会将async方法的剩余部分调度到请求上下文中。ASP.NET获取其中一个处理程序线程(可能是之前执行async方法的同一个线程,也可能不是),计数器递减,然后线程执行async方法。
ASP.NET vNext略有不同;整个框架对异步处理程序提供了更多支持。但基本概念相同。
更多信息:

你能为我澄清一下“ASP.NET如何获取其处理程序线程”不违反“异步编程不需要线程”的问题吗?你的意思是因为你没有创建线程,所以不算违反吗? - kenny
1
当ASP.NET请求异步等待异步操作时,没有使用线程(因此,线程对于异步编程不是必需的)。当方法继续执行时,当然需要使用一些线程来执行代码。异步部分不需要线程。 - Stephen Cleary
1
如果您使用同一线程,如何实现“异步”意味着API不会阻塞调用线程的操作?这是我真正不理解的事情。难道您不需要除主线程之外的另一个执行路径来使其异步运行吗? - Tarik
1
一个线程执行代码。异步操作是您启动的将在以后完成的操作。如果操作不执行任何操作(例如,I/O 操作),则不需要线程。 - Stephen Cleary
1
@TheMuffinMan:只有在UI应用程序中才会出现特定线程的抖动,而不是ASP.NET。通常通过使异步工作更加“粗糙”而不是“啰嗦”来解决这个问题-一般的指导方针是每秒不超过100个连续操作。请记住,使用ConfigureAwait(false)是最佳实践,因此这实际上是每秒100次UI更新-比人类可以处理的要多得多。 - Stephen Cleary
显示剩余4条评论

6

当我第一次看到asyncawait时,我认为它们是异步编程模型的C#语法糖。但我错了,asyncawait不仅仅是如此。它是一种全新的基于任务的异步模式(Task-based Asynchronous Pattern),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. 将发生上下文切换。另一方面,异步不会手动创建任何线程,并且当I/O绑定操作返回时不会进行上下文切换。

这里有一张图片可以帮助理解差异:

enter image description here

这张图来自 MSDN 文章 "ASP.NET 2.0 中的异步页面", 详细解释了 ASP.NET 2.0 中旧的异步工作方式。
关于异步编程模型,请参考 Jeffrey Richter 的文章 "实现 CLR 异步编程模型",他的书 "CLR via C# 3rd Edition" 第27章也有更多细节。

你能否详细解释一下这句话的意思:“异步不会手动创建任何线程,当IO绑定操作返回时也不会进行上下文切换。”? - Tarik
@Tarik,实际上它是线程池而不是异步的,因为大多数CLR APM实现都使用线程池。对于上下文切换,在使用专用线程进行同步IO操作时,如果IO操作比“时间片”长,则线程将阻塞直到完成,然后将发生上下文切换。在异步操作中,请仔细查看图片右侧,第一个线程调用BeginXxx(),它会全部完成并返回到线程池,等待IO操作完成后,它将继续在第二个线程上执行。 - ivenxu

1
假设您正在实现一个 Web 应用程序,每当客户端请求到达您的服务器时,您都需要发出数据库请求。当客户端请求到达时,线程池线程将调用您的代码。如果现在同步发出数据库请求,则线程将阻塞,无限期地等待数据库响应结果。如果在此期间另一个客户端请求进来,则线程池将不得不创建另一个线程,并且当它进行另一个数据库请求时,该线程也会被阻塞。随着越来越多的客户端请求进来,越来越多的线程会被创建,并且所有这些线程都会阻塞等待数据库响应。结果是,您的 Web 服务器会分配大量几乎未被使用的系统资源(线程及其内存)!更糟糕的是,当数据库回复各种结果时,线程变为非阻塞状态并开始执行。但由于您可能有很多正在运行的线程和相对较少的 CPU 核心,Windows 必须频繁进行上下文切换,这会进一步影响性能。这不是实现可扩展应用程序的方法。
为了从文件中读取数据,我现在调用ReadAsync而不是Read。ReadAsync内部分配一个Task对象来表示读取操作的挂起完成。然后,ReadAsync调用Win32的ReadFile函数(#1)。ReadFile分配它的IRP,就像在同步情况下一样初始化它(#2),然后将其传递给Windows内核(#3)。Windows将IRP添加到硬盘驱动程序的IRP队列(#4),但现在,您的线程被允许返回到您的代码,而不是阻塞您的线程(#5、#6和#7)。当然,IRP可能还没有被处理,因此您不能在ReadAsync之后有尝试访问传入的Byte[]字节的代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接