async /等待不同的线程ID

Rob*_*ert 23 c# asynchronous async-await

我最近正在阅读关于async/await的事情,我对以下事实感到困惑:我正在阅读的许多文章/帖子说明在使用异步等待时没有创建新线程(示例).

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

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);

        }
    }
Run Code Online (Sandbox Code Playgroud)

以下代码的输出是:

Main: 8
Main Async: 8
thisIsAsyncStart: 8
thisIsAsyncEnd: 9
Main End: 8
Run Code Online (Sandbox Code Playgroud)

我错过了这一点,或者这个AsyncEnd是否具有与其他操作不同的线程ID?

编辑:

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

引用下面的答案:

Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*ary 35

我建议你阅读我的async介绍帖子,了解asyncawait关键词.特别是,await(默认情况下)将捕获"上下文"并使用该上下文来恢复其异步方法.这个"上下文"是当前的SynchronizationContext(或者TaskScheduler,如果没有SynchronzationContext).

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

正如我在博客上解释的那样,真正的异步操作不会在任何地方"运行".在这种特殊情况下(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);
}
Run Code Online (Sandbox Code Playgroud)
  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或被TaskScheduler捕获await,它将继续在线程池线程上执行.
  9. 线程池线程到达结尾thisIsAsync,完成其任务.
  10. MainAsync继续执行.由于没有捕获的上下文await,它将继续在线程池线程(实际上是正在运行的线程thisIsAsync)上执行.
  11. 线程池线程到达结尾MainAsync,完成其任务.
  12. 主线程从其调用返回Wait并继续执行该Main方法.线程池线程用于继续thisIsAsync并且MainAsync不再需要并返回到线程池.

这里重要的一点是使用了线程池,因为没有上下文.它不是"必要时"自动使用的.如果你在GUI应用程序中运行相同的MainAsync/ thisIsAsync代码,那么你会看到非常不同的线程使用:UI线程有一个SynchronizationContext调度延续回到UI线程,所以所有方法将在同一个UI线程上恢复.

  • 很棒的答案。非常感谢。 (4认同)