不需要线程的情况下理解异步/等待的机制

5

根据MSDN的说法, asyncawait不会创建新线程:

asyncawait关键字不会导致额外的线程被创建。

考虑到这一点,我很难理解一些简单程序的控制流程。下面是我的完整示例。请注意,它需要Dataflow库,你可以从NuGet安装它。

using System;
using System.Threading.Tasks.Dataflow;

namespace TaskSandbox
{
    class Program
    {
        static void Main(string[] args)
        {
            BufferBlock<int> bufferBlock = new BufferBlock<int>();

            Consume(bufferBlock);
            Produce(bufferBlock);

            Console.ReadLine();
        }

        static bool touched;
        static void Produce(ITargetBlock<int> target)
        {
            for (int i = 0; i < 5; i++)
            {
                Console.Error.WriteLine("Producing " + i);
                target.Post(i);
                Console.Error.WriteLine("Performing intensive computation");
                touched = false;
                for (int j = 0; j < 100000000; j++)
                    ;
                Console.Error.WriteLine("Finished intensive computation. Touched: " + touched);
            }

            target.Complete();
        }

        static async void Consume(ISourceBlock<int> source)
        {
            while (await source.OutputAvailableAsync())
            {
                touched = true;
                int received = source.Receive();
                Console.Error.WriteLine("Received " + received);
            }
        }
    }
}

输出:

Producing 0
Performing intensive computation
Received 0
Finished intensive computation. Touched: True
Producing 1
Performing intensive computation
Received 1
Finished intensive computation. Touched: True
Producing 2
Performing intensive computation
Received 2
Finished intensive computation. Touched: False
Producing 3
Performing intensive computation
Received 3
Finished intensive computation. Touched: False
Producing 4
Performing intensive computation
Received 4
Finished intensive computation. Touched: True

这似乎表明在 for 循环运行时,Consume 获得控制权,因为 OutputAvailableAsync 任务已经完成:
for (int j = 0; j < 100000000; j++)
    ;

在线程模型中,这并不令人惊讶。但如果没有涉及其他线程,那么在 for 循环的中间,如何使 Produce 放弃控制权呢?

3
由于这是一个控制台应用程序,所以没有“同步上下文”,这意味着所有来自await调用的回调都会转到SynchronizationContext.Default,即线程池,因此在执行此程序时实际上有两个线程在某些时候运行。为了防止创建额外的线程,您需要创建自定义同步上下文并设置它。如果这样做,您会发现在生产结束之前不会调用“Received”调用,因为在生产过程中放弃控制权。 - Servy
@Servy:如果涉及多个线程,为什么MSDN声称“特别是对于IO绑定操作,这种方法比BackgroundWorker更好,因为...你不必防范竞态条件”?我编辑了我的示例以添加一个简单的竞态条件。 - Matthew
2
@Matthew 使用 await 不一定涉及线程的创建。您可以以不创建线程的方式使用它。您只是还没有这样做。此外请注意,它仅使用线程池线程来运行回调。当线程池线程没有任务时,它并不会阻塞,实际上它什么也不做并被释放回线程池,可以处理另一个请求。这很重要,因为它意味着您不会有100个线程池线程阻塞在那里等待IO完成。 - Servy
2
@Matthew 你可能会发现我的async/await介绍很有帮助。我试图在一篇文章中涵盖所有基础知识和相关细节。 - Stephen Cleary
Stephen Cleary的介绍是我迄今为止阅读过的最好的async/await介绍。他在“下一步”部分中的下一篇文章《异步编程的最佳实践》也是必读之选。 - Alex
显示剩余2条评论
2个回答

2
如果没有额外的线程参与,那么在for循环中如何使Produce让出控制权?谁说没有额外的线程参与呢?你所说的事实是:async和await关键字不会创建额外的线程。这是绝对正确的。你的程序包括以下片段:
target.Post(i);

await source.OutputAvailableAsync())

我的猜测是,调用target.Post(i)source.OutputAvailableAsync()创建了一个线程。 await不会创建线程;所有await做的只是将方法的剩余部分分配为调用返回的任务的继续项,然后将控制返回给调用者。如果该任务生成一个线程来执行其工作,那就是它的事情。 await只是另一种控制流;确实是一种非常复杂的控制流,但仍然是一种控制流。它不是用于创建线程的语法糖;它是用于将继续项分配给任务的语法糖。

4
实际上,这两个调用都不会创建线程。@Servy 是正确的,在使用 await 运算符时(更具体地说,是 await 运算符生成的代码所使用的任务等待器),它使用默认的 SynchronizationContext 在线程池线程上安排方法继续执行。 - Stephen Cleary
@StephenCleary:啊,好的。谢谢你的提醒。 - Eric Lippert

-1

控制处理在同一线程中完成,因此当循环运行时,Consume方法不会运行,反之亦然。如果您使用线程,情况可能并非如此,实际上您希望两者同时运行。

它们在同一线程中的事实并不意味着控制不能从代码的一部分传递到另一部分。.NET(以及其他所有框架,据我所知)都可以平稳处理,每个部分都可以在自己的上下文中运行而没有问题。

但是,在一个线程中运行这两个任务意味着当Consume正在运行时,循环将“挂起”。如果Consume花费太长时间,那正是用户可能感知到的。这就是为什么许多新手窗体编程的程序员会惊讶地发现,一次性向GUI控件填充过多信息会导致他们的窗体挂起并有时变空白 - 刷新屏幕的线程与控制逻辑运行的线程相同,如果您不使用后台工作线程。


什么导致控制权在没有显式地让出控制的情况下传递给“Consume”而不是“Produce”?.NET 是否注意到“Produce”已经运行了很长时间,并将控制权交给“Consume”(类似于线程模型中操作系统的作用)? - Matthew
实际上,我认为Servy在评论中的回答比我的好。 - Geeky Guy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接