C#控制流程等待异步和线程

Joh*_* L. 3 c# multithreading asynchronous async-await

微软表示:"async和await关键字不会导致创建额外的线程.异步方法不需要多线程,因为异步方法不能在自己的线程上运行.该方法在当前同步上下文上运行,并仅在方法处于活动状态时在线程上使用时间.您可以使用Task.Run将CPU绑定的工作移动到后台线程,但后台线程对于只等待结果可用的进程没有帮助."

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

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

让我们假设第一个client.GetByteArrayAsync方法在Task download3 = ProcessURLAsync(..)之前完成,当时第一个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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Sco*_*ain 7

它在不创建新线程的情况下调用函数的方式是主"UI"线程不断地通过工作队列来一个接一个地处理队列中的项目.您可能听到的一个常见术语是"消息泵".

当您执行a await并且您正在从UI线程运行时,一旦调用完成,GetByteArrayAsync新作业将被放入队列中,当它成为该作业时,它将继续该方法的其余代码.

GetByteArrayAsync不要使用线程来完成它的工作,它要求操作系统完成工作并将数据加载到缓冲区然后它等待操作系统告诉它OS已经完成加载缓冲区.当该消息从操作系统进入时,一个新项目进入我之前讨论的那个队列(有点,我稍后再说),一旦它成为该项目,它将复制它从操作系统获得的小缓冲区更大的内部缓冲区并重复该过程.一旦它获得了文件的所有字节,它就会发出信号告知你的代码已经完成,导致你的代码将它继续放到队列中(我在上一段解释的内容).

在谈到GetByteArrayAsync将项目放入队列时我说"有点"的原因是程序中实际上有多个队列.UI有一个,一个用于"线程池",另一个用于"I/O完成端口"(IOCP).线程池和IOCP将生成或重用池中的短期线程,因此可以将此技术称为创建线程,但可用线程在池中空闲,不会创建线程.

您的代码将按原样使用"UI队列",代码GetByteArrayAsync很可能使用线程池队列来完成它的工作,操作系统用来告诉GetByteArrayAsync缓冲区中的数据可用的消息使用IOCP队列.

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

var byteArray = await client.GetByteArrayAsync(url).ConfigureAwait(false);
Run Code Online (Sandbox Code Playgroud)

此设置告诉await"而不是尝试使用SynchronizationContext.Current排队工作(UI队列,如果您在UI线程上)使用"默认" SynchronizationContext(这是线程池队列)

假设第一个"client.GetByteArray Async"方法在"Task download3 = ProcessURLAsync(..)"之前完成,那么它将是"Task download3 = ProcessURLAsync(..)"还是"DisplayResults"将被调用?因为据我所知,他们都会在你提到的队列中.

我将尝试对从鼠标单击到完成发生的所有事件做出明确的事件序列

  1. 您在屏幕上单击鼠标
  2. 操作系统使用来自IOCP池的线程将WM_LBUTTONDOWN消息放入UI消息队列中.
  3. UI消息队列最终会到达该消息,并让所有控件知道它.
  4. Button命名控制startButton接收消息的消息,看到鼠标被放置在其自身,当事件被触发并调用它的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. SendAsync内部做了很多东西,最终导致它从操作系统发送请求从本机DLL下载文件.

到目前为止,没有发生任何"异步",这只是所有正常的同步代码.到目前为止,如果它是同步或异步,则表现完全相同.

  1. 一旦对操作系统发出请求,就SendAsync返回Task当前处于"正在运行"状态的状态.
  2. 稍后在文件中它到达了 response = await sendTask.ConfigureAwait(false);
  3. await检查任务的状态,发现它仍在运行,从而导致在"运行"状态的新任务返回的功能外,还要求任务一旦完成就可以运行一些额外的代码,而是使用线程池做那些额外的代码(因为它使用.ConfigureAwait(false)).
  4. 重复此过程直到最终GetByteArrayAsync返回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线程上,并且在整个过程中尚未将控制权交还给消息泵.

  1. await download1发现任务处于"正在运行"状态并且它要求任务使用SynchronizationContext.Current它运行一些其他代码,然后创建一个Task处于"正在运行"状态并返回它的新任务.
  2. awaitCreateMultipleTasksAsync它的结果看到任务处于"运行"状态,它要求任务使用运行一些额外的代码SynchronizationContext.Current.因为该功能async void只是将控制权返回给消息泵.
  3. 消息泵处理队列中的下一条消息.

好的,得到了​​所有这些?现在我们继续讨论当"工作完成"时会发生什么

一旦你在任何时候执行步骤10操作系统可以使用IOCP发送消息告诉代码它已经完成归档缓冲区,那IOCP线程可以复制数据或者掩码请求线程池线程执行它(我没看足够深,看哪个).

这个过程不断重复,直到所有数据都被下载,一旦完全下载了"额外代码"(委托),步骤12要求任务发送到SynchronizationContext.Post,因为它使用了委托将执行的默认上下文线程池.在该委托的末尾,它将Task具有"正在运行"状态的原始信号发送到已完成状态.

一旦Task<byte[]>在步骤14中返回,在步骤14中等待它SynchronizationContext.Post,它将执行它,该委托将包含类似于的代码

Delegate someDelegate () =>
{
    DisplayResults(url, byteArray);
    SetResultOfProcessURLAsyncTask(byteArray.Length);
}
Run Code Online (Sandbox Code Playgroud)

因为您传入的上下文是UI上下文,所以此委托被放入要由UI处理的消息队列中,UI线程将在有机会时获取它.

一旦ProcessURLAsync用于download1完成,这将导致一个委托,它看起来有点像

Delegate someDelegate () =>
{
    int length2 = await download2;
}
Run Code Online (Sandbox Code Playgroud)

因为您传入的上下文是UI上下文,所以此委托被放入要由UI处理的消息队列中,UI线程将在有机会时获取它.一旦完成,它会排队一个看起来有点像的代表

Delegate someDelegate () =>
{
    int length3 = await download3;
}
Run Code Online (Sandbox Code Playgroud)

因为您传入的上下文是UI上下文,所以此委托被放入要由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();
}
Run Code Online (Sandbox Code Playgroud)

因为您传入的上下文是UI上下文,所以此委托被放入要由UI处理的消息队列中,UI线程将在有机会时获取它.一旦调用"SetTaskForCreateMultipleTasksAsyncDone",它就会排队一个看起来像的委托

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

你的工作终于完成了.

我做了一些重要的简化,并做了一些白色的谎言,使它更容易理解,但这是发生的事情的基本要点.当一个Task完成它的工作时,它将使用它已经在做的线程SynchronizationContext.Post,该帖子将把它放入上下文所用的任何队列中,并将由处理队列的"泵"处理.