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

时间:2016-08-16 19:57:04

标签: 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);
    }
}

2 个答案:

答案 0 :(得分:7)

它在不创建新线程的情况下调用函数的方式是主要的&#34; UI&#34;线程不断地通过工作队列来做和处理队列中的项目一个接一个。您可能听到的一个常见术语是&#34; Message Pump&#34;

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

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

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

您的代码按原样使用&#34; UI队列&#34;,代码GetByteArrayAsync最有可能使用线程池队列来完成它的工作,操作系统使用的消息告诉GetByteArrayAsync缓冲区中可用的数据使用IOCP队列。

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

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

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

  

让我们假设第一个&#34; client.GetByteArray Async&#34;方法完成了   之前&#34;任务download3 = ProcessURLAsync(..)&#34;然后,它会是&#34;任务   download3 = ProcessURLAsync(..)&#34;或&#34; DisplayResults&#34;那将是   调用?因为据我所知,他们都会在   排队你提。

我将尝试制作从鼠标点击到完成所发生的所有事件的明确事件序列

  1. 您在屏幕上点击鼠标
  2. 操作系统使用来自IOCP池的线程将WM_LBUTTONDOWN消息放入UI消息队列中。
  3. UI消息队列最终会收到该消息,并让所有控件知道它。
  4. 名为Button的{​​{1}}控件收到消息消息,发现事件被触发时鼠标位于自身上方并调用其click事件处理程序
  5. 点击事件处理程序调用{​​{1}}
  6. startButton来电startButton_Click
  7. startButton_Click来电CreateMultipleTasksAsync
  8. CreateMultipleTasksAsync来电ProcessURLAsync
  9. ProcessURLAsync最终在内部执行base.SendAsync(request, linkedCts.Token),
  10. client.GetByteArrayAsync(url)在内部做了很多事情,最终导致它从操作系统发送请求从本机DLL下载文件。
  11. 到目前为止,没有什么&#34; async&#34;已经发生了,这只是所有正常的同步代码。到目前为止,如果它是同步或异步,则表现完全相同。

    1. 在向操作系统发出请求后,GetByteArrayAsync会返回当前位于&#34;正在运行&#34;的SendAsync。状态。
    2. 稍后在文件中它会到达SendAsync
    3. Task检查任务的状态,发现它仍然在运行并导致该函数返回一个新的任务在&#34;运行&#34; state,它还要求任务在完成后运行一些额外的代码,但是使用线程池来执行其他代码(因为它使用了response = await sendTask.ConfigureAwait(false);)。
    4. 此过程会一直重复,直到最后await返回&#34;正在运行&#34;中的.ConfigureAwait(false)
    5. 您的GetByteArrayAsync看到返回的Task<byte[]>位于&#34;正在运行&#34;在#34; Running&#34;中使用状态并使函数返回新的await状态,它还要求Task<byte[]>使用Task<int>运行一些额外的代码(因为你没有指定Task<byte[]>),这将导致在运行时将附加代码放入队列我们上次在第3步中看到了。
    6. SynchronizationContext.Current会返回&#34;正在运行&#34;中的.ConfigureAwait(false) state和该任务存储在变量ProcessURLAsync
    7. 针对变量Task<int>download1
    8. 再次重复步骤7-15

      注意:我们仍然在UI线程上,并且在整个过程中还没有将控制权交还给消息泵。

      1. download2它看到任务在&#34; Running&#34;状态并且它要求任务使用download3运行一些额外的代码,然后创建一个新的await download1,它位于&#34; Running&#34;陈述并将其归还。
      2. SynchronizationContext.Current Task await结果显示任务在&#34;正在运行&#34; state并且它要求任务使用CreateMultipleTasksAsync运行一些额外的代码。因为函数是SynchronizationContext.Current,它只是将控制权返回给消息泵。
      3. 消息泵处理队列中的下一条消息。
      4. 好的,得到了​​所有这些?现在我们继续讨论当#34;工作完成后会发生什么&#34;

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

        这个过程不断重复,直到所有数据都被下载,一旦完全下载了&#34;额外代码&#34; (委托)第12步要求将任务发送到SynchronizationContext.Post,因为它使用了委托将由线程池执行的默认上下文。在该委托的最后,它表示返回的原始async void具有&#34; Running&#34;陈述到完成状态。

        在步骤13中返回的Task,等待在步骤14中,它执行了Task<byte[]>,此代理将包含类似于

        的代码
        SynchronizationContext.Post

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

        Delegate someDelegate () => { DisplayResults(url, byteArray); SetResultOfProcessURLAsyncTask(byteArray.Length); } 的{​​{1}}一旦完成,就会导致看起来有点像

        的委托
        ProcessURLAsync

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

        的委托
        download1

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

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

        因为您传入的上下文是UI上下文,此委托被放入要由UI处理的消息队列中,UI线程将在有机会时到达它。一旦&#34; SetTaskForCreateMultipleTasksAsyncDone&#34;被调用它排队一个看起来像

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

        你的工作终于完成了。

        我做了一些重要的简化,并做了一些白色的谎言,使它更容易理解,但这是发生的事情的基本要点。当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(); } 完成它的工作时,它将使用它正在处理的线程来执行Delegate someDelegate () => { resultsTextBox.Text += "\r\n\r\nControl returned to startButton_Click.\r\n"; } ,该帖子将把它放入上下文所用的任何队列中并将被处理通过&#34;泵&#34;处理队列。

答案 1 :(得分:0)

有什么帮助我理解async-await工作的方式是this restaurant metaphor by Eric Lippert。 在面试过程中的某个地方搜索异步等待。

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

不使用async-await,在漫长的处理之后做其他事情并继续原始代码会很麻烦,难以理解和维护。

这就是async-await来救援的时候。使用async-await你的线程不会等到冗长的进程完成。事实上,它记得在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功能会发生什么。因为该函数是异步的,所以我们知道它在某处等待。实际上,如果在没有await的情况下编写异步函数,编译器会发出警告。您的线程执行ReadAsync中的所有代码,直到它到达等待。而不是真正等待你的线程在其调用堆栈中上升,看看它是否可以做其他事情。在上面的示例中,它启动DoSomethingElse()。

一段时间后,你的线程会看到await readTextTask。再一次,它不是真正等待它上升它的堆栈,看看是否有一些代码没有等待。

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

此方法的优点是您的线程等待时间较少,因此您的进程将提前完成。此外,它将使您的呼叫者(包括UI)保持响应,而不会有多线程的开销和困难。

您的代码看起来是顺序的,实际上它不是按顺序执行的。每次满足等待时,将执行未等待的调用堆栈中的某些代码。请注意,尽管它不是顺序的,但它仍然由一个线程完成。

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

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

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。您的线程不会执行任何操作,而是将控制权返回给您的过程,并记住它仍然在Task对象downLoad1中进行一些处理。它再次开始调用ProcessUrlAsync。不等待结果并开始第三次下载。每次记住它仍然在Task对象downLoad2和downLoad3中有所作为。

现在你的进程真的无所事事了,所以等待第一个downLoad完成。

这并不意味着你的线程确实无所事事,它会调用它的调用堆栈来查看是否有任何调用者没有等待并开始处理。在您的示例中,Start_Button_Click正在等待,因此它将转到调用者,这可能是UI。用户界面可能没有等待,所以可以自由地做其他事情。

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

顺便说一句,您可以等待所有任务完成使用Task.WhenAll

,而不是等待三次。
await Task.WhenAll(new Task[] {downLoad1, download2, download3});

另一个帮助我理解async-await的文档是Async And Await by the ever so helpful Stephen Cleary