2016-08-16 26 views
2

Microsoft说:“异步和await关键字不会导致创建额外的线程。异步方法不需要多线程,因为异步方法不会在其自己的线程上运行。该方法在当前同步上下文上运行,并仅在该方法处于活动状态时才在线程上使用时间。您可以使用Task.Run将CPU绑定的工作移动到后台线程,但后台线程无助于只等待结果可用的进程。“C#控制流等待异步和线程

以下是Microsoft使用的Web请求示例用于解释异步和等待的使用。 (https://msdn.microsoft.com/en-us/library/mt674880.aspx)。在问题结尾我粘贴了示例代码的相关部分。

我的问题是,在每个“var byteArray = await client.GetByteArrayAsync(url);”语句之后,控件返回到CreateMultipleTasksAsync方法,然后调用另一个ProcessURLAsync方法。在三次下载被调用后,它开始等待第一个ProcessURLAsync方法完成。但是如果ProcessURLAsync没有在单独的线程中运行,它如何继续执行DisplayResults方法?因为如果它不在不同的线程上,在将控制权返回给CreateMultipleTasksAsync之后,它永远无法完成。你能提供一个简单的控制流程,以便我能理解吗?

假设任务download3 = ProcessURLAsync(..)之前完成了第一client.GetByteArrayAsync方法,正是被称为第一DisplayResults什么时候?

private async void startButton_Click(object sender, RoutedEventArgs e) 
    { 
     resultsTextBox.Clear(); 
     await CreateMultipleTasksAsync(); 
     resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n"; 
    } 


    private async Task CreateMultipleTasksAsync() 
    { 
     // Declare an HttpClient object, and increase the buffer size. The 
     // default buffer size is 65,536. 
     HttpClient client = 
      new HttpClient() { MaxResponseContentBufferSize = 1000000 }; 

     // Create and start the tasks. As each task finishes, DisplayResults 
     // displays its length. 
     Task<int> download1 = 
      ProcessURLAsync("http://msdn.microsoft.com", client); 
     Task<int> download2 = 
      ProcessURLAsync("http://msdn.microsoft.com/en-us/library/hh156528(VS.110).aspx", client); 
     Task<int> download3 = 
      ProcessURLAsync("http://msdn.microsoft.com/en-us/library/67w7t67f.aspx", client); 

     // Await each task. 
     int length1 = await download1; 
     int length2 = await download2; 
     int length3 = await download3; 

     int total = length1 + length2 + length3; 

     // Display the total count for the downloaded websites. 
     resultsTextBox.Text += 
      string.Format("\r\n\r\nTotal bytes returned: {0}\r\n", total); 
    } 


    async Task<int> ProcessURLAsync(string url, HttpClient client) 
    { 
     var byteArray = await client.GetByteArrayAsync(url); 
     DisplayResults(url, byteArray); 
     return byteArray.Length; 
    } 


    private void DisplayResults(string url, byte[] content) 
    { 
     // Display the length of each website. The string format 
     // is designed to be used with a monospaced font, such as 
     // Lucida Console or Global Monospace. 
     var bytes = content.Length; 
     // Strip off the "http://". 
     var displayURL = url.Replace("http://", ""); 
     resultsTextBox.Text += string.Format("\n{0,-58} {1,8}", displayURL, bytes); 
    } 
} 
+0

我想想你可能会发现我的['async' intro](http://blog.stephencleary.com/2012/02/async-and-await.html)有帮助。 –

回答

6

它调用功能,而无需创建一个新的线程的方式是主要的“UI”线程不断通过工作要做,加工项目的队列陆续会在队列中。您可能会听到的一个常见术语是“消息泵”

当你做一个await,你是从UI线程中运行,一旦调用完成,以GetByteArrayAsync一个新的工作将在队列中放,当它变成工作的打开将继续的其余代码方法。

GetByteArrayAsync也不使用线程来完成它的工作,它要求操作系统完成工作并将数据加载到缓冲区,然后等待操作系统告诉它操作系统已完成加载缓冲区。当这个消息从操作系统进入时,一个新的项目进入我之前讨论的队列(稍后我会介绍),一旦它变成该项目轮到它将它从操作系统获得的小缓冲区复制到一个更大的内部缓冲区并重复这个过程。一旦它获得了文件的所有字节,它就会表明它已经完成了你的代码,导致你的代码将它延续到队列中(我解释了上一段的内容)。

我之所以说“有点儿”在谈到GetByteArrayAsync投入项目到队列时,实际上存在在你的程序不止一个队列。有一个用于UI,一个用于“线程池”,另一个用于“I/O完成端口”(IOCP)。线程池和IOCP的会生成或在游泳池重用短暂的线程,所以这technicaly可以被称为创建一个线程,但一个线程可以在任何线程将要创建的池闲置。

你的代码,是将使用“UI队列”,代码GetByteArrayAsync是最有可能使用的线程池队列做的工作,操作系统使用的消息告诉GetByteArrayAsync数据可用在缓冲区中使用IOCP队列。

通过在执行await的行上添加.ConfigureAwait(false),您可以更改代码以从使用UI队列切换到线程池队列。

var byteArray = await client.GetByteArrayAsync(url).ConfigureAwait(false); 

此设定告诉await“而不是试图用SynchronizationContext.Current(如果你是在UI线程的UI队列)排队的工作中使用的‘默认’SynchronizationContext(这是线程池队列)

让我们假设第一 “client.GetByteArray异步” 方法 “任务download3 = ProcessURLAsync(..)” 之前完成 然后,会是 “任务 download3 = ProcessURLAsync(..)” 或“DisplayResults “那会是 援引?因为据我所知,他们都会在你提到的 队列中。

我将努力使从鼠标点击恰好完成的一切事件的明确序列

  1. 你点击屏幕
  2. 操作系统鼠标上使用一个线程从IOCP池在UI消息队列中放置一条WM_LBUTTONDOWN消息。
  3. UI消息队列最终得到该消息,并让所有控件知道它。
  4. 命名startButtonButton控制接收消息的消息,看到鼠标被放置在其自身,当事件被触发并调用它的Click事件处理程序
  5. Click事件处理程序调用startButton_Click
  6. startButton_Click电话CreateMultipleTasksAsync
  7. CreateMultipleTasksAsync电话ProcessURLAsync
  8. ProcessURLAsync调用client.GetByteArrayAsync(url)
  9. GetByteArrayAsync最终在内部做一个base.SendAsync(request, linkedCts.Token),
  10. SendAsyncSendAsync在内部做了一堆东西,最终导致它从操作系统发送请求从本机DLL下载文件。

到目前为止,没有发生任何“异步”,这只是所有正常的同步代码。如果它是同步或异步,那么到此为止的所有内容的行为完全相同。

  1. 一旦向OS发出请求,SendAsync返回当前处于“正在运行”状态的Task
  2. 后来在文件中达到一个response = await sendTask.ConfigureAwait(false);
  3. await检查任务的状态,发现它仍在运行,从而导致在“运行”状态有一个新的任务返回的功能外,还要求该任务在完成后运行一些附加代码,但使用线程池来执行附加代码(因为它使用了.ConfigureAwait(false))。
  4. 重复此过程直到最终GetByteArrayAsync返回“Running”中的Task<byte[]>
  5. await看到返回的Task<byte[]>是在“运行”状态,并导致在“运行”状态的新Task<int>返回功能外,还询问Task<byte[]>运行使用SynchronizationContext.Current一些额外的代码(因为你做了没有指定.ConfigureAwait(false)),这将导致运行时添加的代码放入我们上次在步骤3中看到的队列。
  6. ProcessURLAsync返回一个Task<int>,它处于“正在运行”状态,并且该任务存储在变量download1
  7. 步骤7-15再次得到重复变量download2download3

注意:我们仍然在UI线程上,还没有在这整个过程中,产生控制回消息泵。

  • await download1它看到的任务是在“运行”状态,它要求任务使用SynchronizationContext.Current它,然后创建一个新的Task是在运行一些额外的代码“运行“状态并返回。
  • awaitCreateMultipleTasksAsync的结果表明任务处于“正在运行”状态,并要求任务使用SynchronizationContext.Current运行一些附加代码。因为该函数是async void它只是将控制权返回给消息泵。
  • 消息泵处理队列中的下一条消息。

  • 好吧,明白了吗?现在我们继续讨论“完成工作”时会发生什么

    一旦您在任何时候执行了第10步操作系统可能会使用IOCP发送消息来告诉代码它已完成填写缓冲区,那么IOCP线程可能会复制数据或它的掩码要求一个线程池线程来做到这一点(我看起来不够深刻,看看哪个)。

    此过程不断重复,直到下载完所有数据后,一旦完全下载“额外代码”(代理)步骤12,要求将该任务发送到SynchronizationContext.Post,因为它使用了默认的代理上下文将由线程池执行。在该代表结束时,它将原来的具有“正在运行”状态的Task标记为已完成状态。

    一旦在步骤13中返回的Task<byte[]>,在第14步等待它确实是SynchronizationContext.Post,这代表将包含类似的代码

    Delegate someDelegate() => 
    { 
        DisplayResults(url, byteArray); 
        SetResultOfProcessURLAsyncTask(byteArray.Length); 
    } 
    

    因为你传递的背景是该委托投入获取UI背景要由UI处理的消息队列,当有机会时,UI线程将得到它。

    一旦ProcessURLAsyncdownload1完成,这将导致一个委托,它看起来有点像

    Delegate someDelegate() => 
    { 
        int length2 = await download2; 
    } 
    

    因为你传递的背景是由处理的UI背景下,这个代表把在消息队列中获得用户界面,UI线程会在有机会时得到它。一旦一个完成它确实排队了一个委托,它看起来有点像

    Delegate someDelegate() => 
    { 
        int length3 = await download3; 
    } 
    

    因为你传递了这个委托把在消息队列中获得的UI上下文的上下文由UI进行处理,用户界面线程会在有机会时得到它。一旦协议达成,排队的委托,看起来有点像

    Delegate someDelegate() => 
    { 
        int total = length1 + length2 + length3; 
    
        // Display the total count for the downloaded websites. 
        resultsTextBox.Text += 
         string.Format("\r\n\r\nTotal bytes returned: {0}\r\n", total); 
        SetTaskForCreateMultipleTasksAsyncDone(); 
    } 
    

    因为你传递了这个委托把在消息队列中获得的UI上下文的上下文由UI进行处理,UI线程将当它有机会的时候得到它。一旦“SetTaskForCreateMultipleTasksAsyncDone”被调用时,排队的委托,看起来像

    Delegate someDelegate() => 
    { 
        resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n"; 
    } 
    

    和你的工作是最终完成。

    我做了一些主要的简化,并做了一些白色的谎言,使它更容易理解,但这是发生什么的基本问题。当一个Task完成它的工作时,它将使用它已经处理的线程来执行SynchronizationContext.Post,该帖子将把它放入任何上下文的队列中,并由处理队列的“泵”处理。

    +0

    假设“任务download3 = ProcessURLAsync(..)”之前完成了第一个“client.GetByteArrayAsync”方法的话,会是“任务download3 = ProcessURLAsync(..)”或“DisplayResults”会被调用?因为据我所知,他们都会在你提到的队列中。 –

    +0

    @JohnL。我试图一步一步地经历发生的事情。让我知道,如果有任何点你还没有得到 –

    +0

    感谢这样一个详细和宝贵的解释。因此,既然你说线程处理“有机会的时候”的消息,我可以把它看作:如果GetByteArrayAsync(或者任何异步方法)在调用函数到达await点之前完成(这是“await download1 “在这个例子中),然后在调用方法到达”await download1“之后,异步方法的其余部分(DisplayResults就会被执行),以便线程处于空闲等待状态。我是否正确? –

    0

    帮助我了解异步等待作品的方式是this restaurant metaphor by Eric Lippert。 在面试过程中的某个地方寻找异步等待。

    仅当您的线程有时需要等待很长时间才能完成,如将文件写入磁盘,查询数据库中的数据,从互联网获取信息时,异步等待才有意义。在等待这些操作完成时,您的线程可以自由地执行其他操作。

    在不使用异步等待的情况下,在冗长的处理之后执行其他操作并继续原始代码将非常麻烦并且难以理解和维护。

    这就是当异步等待来救援。使用异步等待您的线程不会等到冗长的过程完成。事实上,它记得在Task对象的长度处理之后仍然需要做某些事情,并开始做其他事情,直到它需要冗长过程的结果。

    在Eric Lippert的比喻中:开始烘烤面包后,厨师不会等到线程启动。相反,他开始煮鸡蛋。

    在这段代码看起来像:

    private async Task MyFunction(...) 
    { 
        // start reading some text 
        var readTextTask = myTextReader.ReadAsync(...) 
        // don't wait until the text is read, I can do other things: 
        DoSomethingElse(); 
        // now I need the result of the reading, so await for it: 
        int nrOfBytesRead = await readTextTask; 
        // use the read bytes 
        .... 
    } 
    

    会发生什么事是你的线程进入ReadAsync功能。因为函数是异步的,所以我们知道有一个地方在等待它。事实上,如果你没有等待写一个异步函数,你的编译器会发出警告。你的线程执行ReadAsync中的所有代码,直到达到await。而不是真的等待你的线程在其调用堆栈中上升,看看它是否可以做其他事情。在上面的例子中,它启动DoSomethingElse()。

    过了一段时间你的线程看到await readTextTask。再次,而不是真的在等待它上涨,看看是否有一些代码没有等待。

    它继续这样做,直到每个人都在等待。然后,只有当你的线程真的不能再做任何事情时,它才开始等待直到ReadAsync的等待完成。

    这种方法的优点是你的线程会等待较少,因而你的过程将过早结束。除此之外,它还可以让您的调用者(包括UI)保持响应,而不会产生多线程的开销和困难。

    您的代码看起来是连续的,实际上它不是按顺序执行的。每次等待时,都会执行调用堆栈中不等待的代码。请注意,虽然它不是顺序的,但它仍然全部由一个线程完成。

    请注意,这一切仍然是单线程。线程一次只能做一件事,所以当你的线程忙于做一些繁重的计算时,你的调用者不能做任何事情,并且你的程序在你的线程完成计算之前仍然不会响应。异步等待不会帮助你与THEAD

    这就是为什么你看到耗时的过程在一个单独的线程启动与使用Task.Run的awaitable任务。这将释放你的线程来做其他事情。当然这个方法只有在你的线程在等待计算完成时还有别的事情要做时才有意义,并且如果开始一个新线程的开销比自己进行计算的成本更低。

    private async Task<string> ProcessFileAsync() 
    { 
        var calculationTask = Task.Run(() => HeavyCalcuations(...)); 
        var downloadTask = downloadAsync(...); 
    
        // await until both are finished: 
        await Task.WhenAll(new Task[] {calculationTask, downloadTak}); 
        double calculationResult = calculationTask.Result; 
        string downloadedText = downloadTask.Result; 
    
        return downloadedText + calculationResult.ToString(); 
    } 
    

    现在回到你的问题。

    某处在第一ProcessUrlAsync是AWAIT。而不是无所事事,你的线程将控制权交还给你的程序,并记住它还有一些处理任务对象downLoad1。它开始再次调用ProcessUrlAsync。不等待结果并开始第三次下载。每次记住它在Task对象downLoad2和downLoad3中还有某些事情要做。现在

    你真的过程无关了,所以它等待第一个下载完成。

    这并不意味着你的线程真正在做什么,它上升它的调用堆栈,看看是否有任何来电者不是等待并开始处理。在你的例子中,Start_Button_Click正在等待,所以它转到调用者,这可能是UI。 UI可能不在等待,所以可以自由地做其他事情。

    所有下载完成后,您的线程将继续显示结果。

    顺便说一句,而不是等待三次,你可以等待所有任务使用Task.WhenAll

    await Task.WhenAll(new Task[] {downLoad1, download2, download3}); 
    

    另一个文件,帮了我很多理解异步的await完成是Async And Await by the ever so helpful Stephen Cleary