关于线程的混淆以及异步方法在C#中是否真正异步

Fla*_*ack 48 .net c# asynchronous

我正在阅读async/await,什么时候Task.Yield可能有用,并且发现了这篇文章. 我从那篇文章中得到了关于下面的问题:

使用async/await时,无法保证在执行await时调用的方法FooAsync()实际上是异步运行的.内部实现可以使用完全同步的路径自由返回.

这对我来说有点不清楚,可能是因为我头脑中的异步定义没有排成一行.

在我看来,由于我主要做UI开发,异步代码是不在UI线程上运行的代码,而是在其他一些线程上.我想在我引用的文本中,如果一个方法阻塞在任何线程上(即使它是一个线程池线程),它也不是真正的异步.

题:

如果我有一个长时间运行的任务是CPU绑定的(假设它做了很多硬数学运算),那么异步运行该任务必须阻塞一些线程吗?有些东西实际上要做数学.如果我等待它,那么一些线程就会被阻止.

什么是真正的异步方法的例子,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I/O操作,因此没有阻止任何线程?

Eri*_*ert 109

这对我来说有点不清楚,可能是因为我头脑中的异步定义没有排成一行.

要求澄清你好.

在我看来,由于我主要做UI开发,异步代码是不在UI线程上运行的代码,而是在其他一些线程上.

这种看法很常见但却是错误的.不要求异步代码在任何第二个线程上运行.

想象一下,你正在做早餐.你在烤面包机里放了一些烤面包,当你在等待吐司的时候,你从昨天开始收到你的邮件,支付一些账单,嘿,吐司就出现了.你完成支付账单然后去烤面包.

你在哪里雇用第二个工人来看烤面包机?

你没有.线程是工人.异步工作流可以在一个线程上发生.异步工作流程的目的是避免雇用更多的工人,如果你可以避免它.

如果我有一个长时间运行的任务是CPU绑定的(假设它做了很多硬数学运算),那么异步运行该任务必须阻塞一些线程吗?有些东西实际上要做数学.

在这里,我会给你一个难以解决的问题.这是一列100个数字; 请手动添加.因此,您将第一个添加到第二个并进行总计.然后将运行总计添加到第三个并获得总计.然后,哦,地狱,第二页的数字丢失了.记住你在哪里,然后去做一些吐司.哦,当烤面包敬酒时,一封信到了剩下的号码.当你完成涂抹吐司时,继续加上这些数字,并记得下次有空闲时刻吃吐司.

您雇用另一名工人添加数字的部分在哪里? 计算上昂贵的工作不需要是同步的,并且不需要阻塞线程.使计算工作可能异步的事情是能够阻止它,记住你在哪里,去做别的事情,记住在那之后做什么,然后从你离开的地方继续.

现在肯定可以聘请第二名工人,除了增加数字之外什么都不做,然后被解雇.你可以问那个工人"你做完了吗?" 如果答案是否定的,你可以去做三明治,直到它们完成.这样你和工人都很忙.但是并不要求异步涉及多个工人.

如果我等待它,那么一些线程就会被阻止.

不不不.这是你误解中最重要的部分.await并不意味着"以异步方式开始这项工作". await表示"我在这里有一个异步生成的结果,可能无法使用.如果它不可用,请在此线程上找到其他一些工作,这样我们就不会阻塞该线程.等待你刚刚说的相反.

什么是真正的异步方法的例子,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I/O操作,因此没有阻止任何线程?

异步工作通常涉及自定义硬件或多线程,但它不需要.

不要考虑工人.考虑工作流程.异步的本质是将工作流分解为一小部分,这样您就可以确定这些部分必须发生的顺序,然后依次执行每个部分,但允许相互之间没有依赖关系的部分进行交错.

在异步工作流程中,您可以轻松检测工作流中表示部件之间依赖关系的位置.这些部件标有await.这就是await:后面的代码取决于正在完成的工作流的这一部分,所以如果它没有完成,去找一些其他任务要做,并在完成任务后再回到这里.重点是让工人保持工作,即使在将来需要产生结果的世界中也是如此.

  • 我发现我们在"正在做东西"(如中断和DPC)的概念上创建了这个线程抽象,这有点有趣,现在我们已经构建了一个基于线程的"正在做东西"的抽象. (10认同)
  • AFAIK JavaScript既是单线程又强调异步. (5认同)
  • @immibis中断和DPC是如何抽象的?它们是工具,而不是抽象.`Task`是一个抽象. (2认同)

Ste*_*ary 26

我正在阅读async/await

我可以推荐我的async介绍吗?

当Task.Yield可能有用时

几乎从不.我发现它在进行单元测试时偶尔会有用.

在我看来,由于我主要做UI开发,异步代码是不在UI线程上运行的代码,而是在其他一些线程上.

异步代码可以是无线程的.

我想在我引用的文本中,如果一个方法阻塞在任何线程上(即使它是一个线程池线程),它也不是真正的异步.

我会说那是对的.我对不阻塞任何线程(并且不同步)的操作使用术语"真正的异步".我也使用该操作的术语"假异步" 出现异步但只工作方式,因为它们运行或阻塞线程池中的线程.

如果我有一个长时间运行的任务是CPU绑定的(假设它做了很多硬数学运算),那么异步运行该任务必须阻塞一些线程吗?有些东西实际上要做数学.

是; 在这种情况下,您可能希望使用同步API定义该工作(因为它是同步工作),然后您可以使用UI线程调用它Task.Run,例如:

var result = await Task.Run(() => MySynchronousCpuBoundCode());
Run Code Online (Sandbox Code Playgroud)

如果我等待它,那么一些线程就会被阻止.

没有; 线程池线程将用于运行代码(实际上未被阻止),并且UI线程异步等待该代码完成(也未被阻止).

什么是真正的异步方法的例子,它们将如何实际工作?

NetworkStream.WriteAsync(间接地)要求网卡写出一些字节.没有线程负责一次写出一个字节并等待写入每个字节.网卡处理所有这些.当网卡完成所有字节的写入后,它(最终)完成从中返回的任务WriteAsync.

这些是否仅限于利用某些硬件功能的I/O操作,因此没有阻止任何线程?

虽然I/O操作很简单,但并非完全如此.另一个相当简单的例子是计时器(例如Task.Delay).虽然您可以围绕任何类型的"事件"构建真正的异步API.


Cor*_*son 10

当您使用async/await时,无法保证在您等待FooAsync()时调用的方法实际上是异步运行的.内部实现可以使用完全同步的路径自由返回.

这对我来说有点不清楚,可能是因为我头脑中的异步定义没有排成一行.

这只是意味着在调用异步方法时有两种情况.

第一个是,在将任务返回给您后,操作已经完成 - 这将是一个同步路径.第二个是操作仍在进行中 - 这是异步路径.

考虑这段代码,它应该显示这两个路径.如果密钥位于缓存中,则会同步返回该密钥.否则,启动异步操作,调用数据库:

Task<T> GetCachedDataAsync(string key)
{
    if(cache.TryGetvalue(key, out T value))
    {
        return Task.FromResult(value); // synchronous: no awaits here.
    }

    // start a fully async op.
    return GetDataImpl();

    async Task<T> GetDataImpl()
    {
        value = await database.GetValueAsync(key);
        cache[key] = value;
        return value;
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,通过理解,您可以推断出理论上的调用database.GetValueAsync()可能具有类似的代码并且本身能够同步返回:因此即使您的异步路径也可能最终同步运行100%.但是你的代码不需要关心:async/await无缝地处理这两种情况.

如果我有一个长时间运行的任务是CPU绑定的(假设它做了很多硬数学运算),那么异步运行该任务必须阻塞一些线程吗?有些东西实际上要做数学.如果我等待它,那么一些线程就会被阻止.

阻塞是一个明确定义的术语 - 它意味着你的线程在等待某些东西(I/O,互斥体等)时已经产生了它的执行窗口.所以你的数学线程不被认为是阻塞的:它实际上是在执行工作.

什么是真正的异步方法的例子,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I/O操作,因此没有阻止任何线程?

"真正的异步方法"将是一个根本不会阻止的方法.它通常最终涉及I/O,但是await当你想要当前的线程用于其他东西时(例如在UI开发中)或当你试图引入并行性时,它也可能意味着繁重的数学代码:

async Task<double> DoSomethingAsync()
{
    double x = await ReadXFromFile();

    Task<double> a = LongMathCodeA(x);
    Task<double> b = LongMathCodeB(x);

    await Task.WhenAll(a, b);

    return a.Result + b.Result;
}
Run Code Online (Sandbox Code Playgroud)


小智 6

这个主题相当广泛,可能会出现一些讨论.但是,在C#中使用asyncawait被认为是异步编程.但是,异步的工作原理是完全不同的讨论.在.NET 4.5之前,没有async和await关键字,开发人员必须直接针对Task Parallel Librar y(TPL)进行开发.在那里,开发人员可以完全控制何时以及如何创建新任务甚至线程.然而,这有一个缺点,因为不是真正的这个主题的专家,应用程序可能会遇到严重的性能问题和由于线程之间的竞争条件等导致的错误.

从.NET 4.5开始,引入了异步和等待关键字,采用了一种新的异步编程方法.async和await关键字不会导致创建其他线程.异步方法不需要多线程,因为异步方法不能在自己的线程上运行.该方法在当前同步上下文上运行,并仅在方法处于活动状态时在线程上使用时间.您可以使用Task.Run将受CPU限制的工作移动到后台线程,但后台线程对于等待结果可用的进程没有帮助.

在几乎所有情况下,基于异步的异步编程方法优于现有方法.特别是,对于IO绑定操作,此方法优于BackgroundWorker,因为代码更简单,您无需防范竞争条件.您可以在此处阅读有关此主题的更多信息.

我不认为自己是C#黑带,而一些经验丰富的开发人员可能会提出进一步的讨论,但作为一项原则,我希望我能够回答你的问题.

  • 虽然信息量不足以回答OP的问题.OP的问题,IMO,具体到足以在C#异步编程中包含最近历史摘要的答案以及在使用Async的C#异步编程中将OP引用到MSDN的链接不是OP正在寻找的答案,我认为很明显,即使在C#异步编程与异步上阅读MSDN之后,OP问题仍可能仍然存在 (4认同)

The*_*aot 6

异步并不意味着并行

异步仅意味着并发.实际上,即使使用显式线程也不能保证它们会同时执行(例如,当线程对同一个内核具有亲缘关系时,或者更常见的情况是当机器中只有一个内核开始时).

因此,您不应期望异步操作与其他操作同时发生.异步只意味着它会发生,最终在另一个时间(a(希腊)=没有,syn(希腊)=在一起,khronos(希腊)=时间.=> 异步 =不同时发生).

注意:异步性的想法是,在调用时,您不关心代码实际运行的时间.如果可能,这允许系统利用并行性来执行操作.它甚至可能立即运行.它甚至可能发生在同一个线程上......稍后会发生.

当你await进行异步操作时,你正在创建并发(com(latin)= together,currere(latin)= run.=>"Concurrent"= 一起运行).这是因为您要求异步操作在继续之前完成.我们可以说执行收敛了.这类似于连接线程的概念.


当异步不能并行时

当您使用async/await时,无法保证在您等待FooAsync()时调用的方法实际上是异步运行的.内部实现可以使用完全同步的路径自由返回.

这可以通过三种方式实现:

  1. 可以await在返回的任何东西上使用Task.当你收到Task它可能已经完成.

    然而,仅此一点并不意味着它同步运行.实际上,它建议它以异步方式运行并在获得Task实例之前完成.

    请记住,您可以await完成已完成的任务:

    private static async Task CallFooAsync()
    {
        await FooAsync();
    }
    
    private static Task FooAsync()
    {
        return Task.CompletedTask;
    }
    
    private static void Main()
    {
        CallFooAsync().Wait();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    此外,如果async方法没有await,它将同步运行.

    注意:正如您所知,返回a的方法Task可能正在网络上等待,或者在文件系统上等等......这样做并不意味着启动新内容Thread或将内容排入队列ThreadPool.

  2. 在由单个线程处理的同步上下文中,结果将是Task同步执行,但有一些开销.这是UI线程的情况,我将更多地讨论下面发生的事情.

  3. 可以编写自定义TaskScheduler来始终同步运行任务.在同一个线程上,它执行调用.

    注意:最近我编写了一个SyncrhonizationContext在单个线程上运行任务的自定义.您可以在创建(System.Threading.Tasks.)任务计划程序中找到它.它会导致这样TaskScheduler的调用FromCurrentSynchronizationContext.

    默认情况下TaskScheduler会将调用排入队列ThreadPool.然而,当你等待操作时,如果它没有运行ThreadPool它将尝试将其从中删除ThreadPool并在内联运行(在等待的同一线程上......线程正在等待,所以它不忙) .

    注意:一个值得注意的例外是Task标有LongRunning.LongRunning Tasks将在一个单独的线程上运行.


你的问题

如果我有一个长时间运行的任务是CPU绑定的(假设它做了很多硬数学运算),那么异步运行该任务必须阻塞一些线程吗?有些东西实际上要做数学.如果我等待它,那么一些线程就会被阻止.

如果你正在进行计算,它们必须在某个线程上发生,那部分是正确的.

然而,对美asyncawait是等待的线程没有被阻塞(稍后更多).然而,通过将等待的任务安排在正在等待的同一线程上运行,很容易让自己陷入困境,从而导致同步执行(这在UI线程中很容易出错).

其中一个关键特征async,并await为他们采取了SynchronizationContext从主叫方.对于导致使用默认值的大多数线程TaskScheduler(如前所述,使用默认值ThreasPool).但是,对于UI线程,它意味着将任务发布到消息队列中,这意味着它们将在UI线程上运行.这样做的好处是您不必使用InvokeBeginInvoke访问UI组件.

我进入之前,如何await一个Task从UI线程不阻止它,我要指出,这是可能实现TaskScheduler.如果你是await一个Task,你不会阻止你的线程或有它进入空闲状态,而不是你让你的线程选择另一个Task等待执行的人.当我向.NET 2.0后台移植任务时,我试验了这个.

什么是真正的异步方法的例子,它们将如何实际工作?这些是否仅限于利用某些硬件功能的I/O操作,因此没有阻止任何线程?

您似乎混淆异步不阻塞线程.如果您想要的是.NET中不需要阻塞线程的异步操作的示例,那么您可能会发现易于掌握的方法是使用continuation而不是await.对于需要在UI线程上运行的延续,您可以使用TaskScheduler.FromCurrentSynchronizationContext.

不要实现奇特的旋转等待.而我的意思是使用Timer,Application.Idle或类似的东西.

当你使用时,async你告诉编译器以允许破坏它的方式重写方法的代码.结果类似于continuation,语法更方便.当线程到达awaitTask将被安排,而线程是自由后的电流继续async调用(该方法的).当Task完成后,延续(后await)计划.

对于UI线程,这意味着一旦到达await,就可以继续处理消息.等待Task完成后,await将安排继续(之后).因此,到达await并不意味着阻止线程.

然而盲目地添加async并且await不会解决所有问题.

我向你提交了一个实验.获取一个新的Windows窗体应用程序,插入a Button和a TextBox,然后添加以下代码:

    private async void button1_Click(object sender, EventArgs e)
    {
        await WorkAsync(5000);
        textBox1.Text = @"DONE";
    }

    private async Task WorkAsync(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }
Run Code Online (Sandbox Code Playgroud)

它会阻止UI.如前所述,会发生的情况是await自动使用SynchronizationContext调用者线程.在这种情况下,这是UI线程.因此,WorkAsync将在UI线程上运行.

这是发生的事情:

  • UI线程获取单击消息并调用click事件处理程序
  • 在click事件处理程序中,UI线程到达 await WorkAsync(5000)
  • WorkAsync(5000) (并安排其继续)被安排在当前同步上下文上运行,这是UI线程同步上下文...这意味着它发布消息来执行它
  • UI线程现在可以自由处理更多消息
  • UI线程选择要执行的消息WorkAsync(5000)并安排其继续
  • UI线程调用WorkAsync(5000)continuation
  • WorkAsync,UI线程运行Thread.Sleep.用户界面现在无法响应5秒钟.
  • 继续计划要运行的其余click事件处理程序,这是通过为UI线程发布另一个消息来完成的
  • UI线程现在可以自由处理更多消息
  • UI线程选择要在click事件处理程序中继续的消息
  • UI线程更新文本框

结果是同步执行,带有开销.

是的,你应该使用Task.Delay.这不是重点; 考虑Sleep进行一些计算.关键是只使用asyncawait处处都不会给你一个自动并行的应用程序.选择你想在后台线程上ThreadPool运行什么(例如在...上)以及你想在UI线程上运行什么要好得多.

现在,尝试以下代码:

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => Work(5000));
        textBox1.Text = @"DONE";
    }

    private void Work(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }
Run Code Online (Sandbox Code Playgroud)

您会发现await不会阻止UI.这是因为在这种情况下Thread.Sleep现在正在运行的ThreadPool感谢Task.Run.并且由于button1_Click存在async,一旦代码到达awaitUI线程就可以继续工作.完成之后Task,代码将在await感谢编译器重写方法之后恢复,以准确地允许.

这是发生的事情:

  • UI线程获取单击消息并调用click事件处理程序
  • 在click事件处理程序中,UI线程到达 await Task.Run(() => Work(5000))
  • Task.Run(() => Work(5000)) (并安排其继续)被安排在当前同步上下文上运行,这是UI线程同步上下文...这意味着它发布消息来执行它
  • UI线程现在可以自由处理更多消息
  • UI线程选择要执行的消息Task.Run(() => Work(5000))并在完成后安排其继续
  • UI线程调用Task.Run(() => Work(5000))continuation,这将在ThreadPool
  • UI线程现在可以自由处理更多消息

ThreadPool完成后,继续将安排Click事件处理程序的其余部分来看,这是通过发布其他消息的UI线程中完成的.当UI线程选择要在click事件处理程序中继续的消息时,它将更新文本框.