async /等待不同的线程ID

时间:2015-11-20 07:59:52

标签: c# asynchronous async-await

我最近正在阅读有关async / await的内容,而且我对以下事实感到困惑:我正在阅读的许多文章/帖子表明在使用async await(§8 Encoding of Pixel, Overlay and Waveform Data)时未创建新线程。

我创建了一个简单的控制台应用来测试它

class Program
    {
        static  void Main(string[] args)
        {
            Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
            MainAsync(args).Wait();
            Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);

            Console.ReadKey();
        }


        static async Task MainAsync(string[] args)
        {
            Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);

            await thisIsAsync();
        }

        private static async Task thisIsAsync()
        {
            Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(1);
            Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);

        }
    }

以下代码的输出是:

Main: 8
Main Async: 8
thisIsAsyncStart: 8
thisIsAsyncEnd: 9
Main End: 8

我是否遗漏了这一点,或者这个AsyncEnd的线程ID与其他动作不同?

修改

我已根据以下答案中的建议更新了代码await Task.Delay(1),但我仍然看到相同的结果。

从以下答案中引用:

Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously

如果没有创建其他线程,我想知道asynchronously部分在哪里运行? 如果它在同一个线程上运行,不应该由于长I / O请求而阻塞它,或者编译器是否足够聪明,如果它花费太长时间将该操作移动到另一个线程,并且毕竟使用了新线程? / p>

4 个答案:

答案 0 :(得分:29)

我建议您阅读我的async intro帖子,了解asyncawait个关键字。特别是,await(默认情况下)将捕获“上下文”并使用该上下文来恢复其异步方法。此“上下文”是当前SynchronizationContext(或TaskScheduler,如果没有SynchronzationContext)。

  

如果没有创建其他线程,我想知道异步部分在哪里运行?如果它在同一个线程上运行,不应该由于长I / O请求而阻塞它,或者编译器是否足够聪明,如果它花费太长时间将该操作移动到另一个线程,并且毕竟使用了新线程? / p>

我在博客上解释truly asynchronous operations do not "run" anywhere。在这种特殊情况下(Task.Delay(1)),异步操作基于计时器,在某处执行Thread.Sleep时阻塞的线程。大多数I / O都以相同的方式完成。例如,HttpClient.GetAsync基于重叠(异步)I / O,在某处等待HTTP下载完成的线程被阻塞。

一旦了解await如何使用其上下文,就可以更轻松地浏览原始代码:

static void Main(string[] args)
{
  Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
  MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();"
  Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);

  Console.ReadKey();
}

static async Task MainAsync(string[] args)
{
  Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
  await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;"
}

private static async Task thisIsAsync()
{
  Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
  await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;"
  Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
  1. 主线程开始执行Main并调用MainAsync
  2. 主线程正在执行MainAsync并调用thisIsAsync
  3. 主线程正在执行thisIsAsync并调用Task.Delay
  4. Task.Delay做它的事情 - 启动一个计时器和诸如此类的东西 - 并返回一个不完整的任务(注意Task.Delay(0)将返回一个完成的任务,这会改变行为)。
  5. 主线程返回thisIsAsync并等待Task.Delay返回的任务。由于任务不完整,因此会从thisIsAsync返回不完整的任务。
  6. 主线程返回MainAsync并等待thisIsAsync返回的任务。由于任务不完整,因此会从MainAsync返回不完整的任务。
  7. 主线程返回Main并对Wait返回的任务调用MainAsync。这将阻止主线程,直到MainAsync完成。
  8. Task.Delay设置的计时器关闭时,thisIsAsync将继续执行。由于SynchronizationContext没有捕获TaskSchedulerawait,因此它将继续在线程池线程上执行。
  9. 线程池线程到达thisIsAsync的末尾,完成其任务。
  10. MainAsync继续执行。由于没有await捕获的上下文,它继续在线程池线程上执行(实际上是运行thisIsAsync的相同线程)。
  11. 线程池线程到达MainAsync的末尾,完成其任务。
  12. 主线程从其Wait调用返回并继续执行Main方法。用于继续thisIsAsyncMainAsync的线程池线程不再需要并返回到线程池。
  13. 这里重要的一点是线程池使用,因为没有上下文。它不是“必要时”自动使用的。如果您要在GUI应用程序中运行相同的MainAsync / thisIsAsync代码,那么您将看到非常不同的线程使用情况:UI线程有一个SynchronizationContext,它将连续调度重新安排回UI线程,所以所有方法都将在同一个UI线程上恢复。

答案 1 :(得分:1)

使用async制作方法并不意味着它会创建另一个线程。如果RunTime看到您的await方法中使用async调用的方法延迟,它退出该方法并在等待方法完成后等待,然后用另一个线程继续该方法。尝试将您的Task.Delay(2000)更改为Task.Delay(0),您将看到它没有创建一个新的线程

RunTime 会计算它,如果它需要创建它会创建,如果不是 - 不。我尝试你的例子0 ms并得到所有相同的线程:

Main: 1
Main Async: 1
thisIsAsyncStart: 1
thisIsAsyncEnd: 1
Main End: 1

Taken from Stephen Toub's blog

  

“异步”关键字

     

“async”关键字在应用于方法时的作用是什么?

     

当您使用“async”关键字标记方法时,您真的在说   编译器有两件事:

     
      
  1. 您告诉编译器您希望能够在方法中使用“await”关键字(您可以使用await关键字if   并且只有当它所在的方法或lambda被标记为async时)。这样做   所以,你告诉编译器使用状态编译方法   机器,这样该方法将能够暂停然后恢复   在等待点异步。
  2.   
  3. 您告诉编译器“解除”方法的结果或可能出现在返回类型中的任何异常。对于一种方法   返回任务或任务,这意味着任何返回的值或异常   在方法中未处理的内容存储在结果任务中。   对于返回void的方法,这意味着任何异常都是   通过任何方式传播到调用者的上下文   “SynchronizationContext”在方法时是最新的   初始调用。
  4.         

    在方法上使用“async”关键字是否强制该方法的所有调用都是异步的?

         

    没有。当您调用标记为“async”的方法时,它开始运行   同步在当前线程上。所以,如果你有同步   返回void的方法以及所做的更改方法都将其标记为   “async”,该方法的调用仍将同步运行。这个   无论您是否将返回类型保留为“void”或,都是如此   将其更改为“任务”。同样,如果你有一个同步方法   返回一些TResult,你所做的只是将其标记为“异步”并进行更改   返回类型为“任务”,该方法的调用仍然是   同步运行。

         

    将方法标记为“async”不会影响方法是否运行   同步或异步完成。相反,它使...   方法被分成多个部分,其中一些可能会运行   异步,这样该方法可以异步完成。该   这些部分的边界只能在您明确编码的地方出现   一个使用“await”关键字,所以如果“await”在a中根本不被使用   方法的代码,只有一件,因为那件就会   开始同步运行,它(以及它的整个方法)将   同步完成。

答案 2 :(得分:0)

我想知道完全一样。对我来说,MSDN的解释是矛盾的:

  

async和await关键字不会导致其他线程   创建。异步方法不需要多线程,因为异步   方法不能在自己的线程上运行

MSDN: Asynchronous Programming with async and await

  

await表达式不会阻止正在执行的线程。 [..]任务完成后,调用继续执行,异步方法的执行从中断处继续执行。

await (C#-Referenz)

我不明白如何在不使用额外线程的情况下阻止原始线程。另外,“调用”措辞表明在某处以某种方式使用的多个线程。

然后我意识到,一切都写得正确,没有使用任何其他线程这些关键字。通过设计Task类来提供可能使用不同线程的机制 - 或者不是。

Task.Delay()方法stephen-cleary beautifully explained这些机制的同时,我扩展了MSDN示例以了解awaitTask.Run()的行为方式:

private async void ds_StartButton_Click(object sender, EventArgs e)
{
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started MSDN Example ..." + Environment.NewLine);

    // Call the method that runs asynchronously.
    string result = await WaitAsynchronouslyAsync();

    // Call the method that runs synchronously.
    //string result = await WaitSynchronously ();

    // Do other Schdaff
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #1 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #2 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #3 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #4 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #5 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #6 ..." + Environment.NewLine);
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #7 ..." + Environment.NewLine);

    // Display the result.
    textBox1.Text += result;
}

// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window 
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Entered WaitAsynchronouslyAsync()");
    await Task.Delay(10000);
    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Task.Delay done, starting random string generation now ...");

    await Task.Run(() => LongComputation());

    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Leaving WaitAsynchronouslyAsync() ...");
    return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Example." + Environment.NewLine;
}

// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
    // Add a using directive for System.Threading.
    Thread.Sleep(10000);
    return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Bad Ass Example." + Environment.NewLine;
}

private void ds_ButtonTest_Click(object sender, EventArgs e)
{
    textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started Test ..." + Environment.NewLine);
    Task<string> l_Task = WaitAsynchronouslyAsync();
    //WaitAsynchronouslyAsync();

    //textBox1.AppendText(l_Result);
}

private void LongComputation()
{
    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Generating random string ...");

    string l_RandomString = GetRandomString(10000000);

    Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Random string generated.");
}

/// <summary>Get random string with specified length</summary>
/// <param name="p_Length">Requested length of random string</param>
/// <param name="p_NoDots">Use case of this is unknown, but assumed to be importantly needed somewhere. Defaults to true therefore.
/// But due to huge performance implication, added this parameter to switch this off.</param>
/// <returns>Random string</returns>
public static string GetRandomString(int p_Length, bool p_NoDots = true)
{
    StringBuilder l_StringBuilder = new StringBuilder();
    string l_RandomString = string.Empty;

    while (l_StringBuilder.Length <= p_Length)
    {
        l_RandomString = (p_NoDots ? System.IO.Path.GetRandomFileName().Replace(".", string.Empty) : System.IO.Path.GetRandomFileName());
        l_StringBuilder.Append(l_RandomString);
    }

    l_RandomString = l_StringBuilder.ToString(0, p_Length);
    l_StringBuilder = null;

    return l_RandomString;
}

从输出中可以看出, 使用了多个线程 - 不是async/await,而是Task.Run()

04.11.2016 12:38:06 [10] Entered WaitAsynchronouslyAsync()
04.11.2016 12:38:17 [10] Task.Delay done, starting random string generation now ...
04.11.2016 12:38:17 [12] Generating random string ...
04.11.2016 12:38:21 [12] Random string generated.
04.11.2016 12:38:21 [10] Leaving WaitAsynchronouslyAsync() ...

这就像往常一样,但我个人需要这个明确的例子来了解正在发生的事情,并将async/await完成的工作与Task完成的工作分开。

答案 3 :(得分:0)

对你的问题的解释非常好 https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

  

调用控制台应用程序的Main方法时,   SynchronizationContext.Current将返回null。这意味着如果   您在控制台应用程序中调用异步方法,除非您这样做   特别的东西,你的异步方法不会有线程   affinity:这些异步方法中的延续可以   最终在“任何地方”运行。