异步/等待是否使用Task.Run异步启动一个新线程?

53

我已经阅读了很多文章,但仍然无法理解这部分内容。

考虑以下代码:

    private async void button1_Click(object sender, EventArgs e)
    {
        await Dosomething();
    }

    private async Task<string> Dosomething()
    {
        await Task.Run((() => "Do Work"));
        return "I am done";
    }

第一个问题:

当我点击按钮时,它将调用 DoSomething 并等待一个任务,该任务通过调用 Task.Run(如果我没有弄错的话)从线程池创建一个线程,并且所有这些都以异步方式运行。所以我成功地创建了一个线程来完成我的工作,但是以异步方式进行操作?但请考虑,我不需要任何结果返回,我只想完成工作而不获取任何结果,是否真的需要使用 async/await,如果需要,如何操作?

第二个问题:

在异步运行线程时,它是如何工作的?它是在主 UI 上但在单独的线程上运行还是在单独的线程上异步运行在该方法内部?


我对async/await并不是很熟悉,但我相信是否使用新线程取决于操作系统的决定 - 也就是说,不能保证在新线程上执行。 - Tim
2
@Tim Nit:如果被告知这样做,操作系统将启动一个新线程。任何相关的线程池等都在.NET本身中发生。 - user2864740
@user2864740 - 谢谢您的澄清 :) - Tim
1
async/await 永远不会创建新线程 - 否则我们将不得不为每个 awaited 调用后的所有内容编写线程安全的代码,这正是 async/await 尝试避免的。无论 awaited 方法本身是否生成新线程,async/await 都与此无关,awaited 调用始终在调用它的同一线程上运行。 - markmnl
1
@markmnl - 然而,任何阅读您在此处的评论的人都应该阅读您和Matias之间的讨论在他的答案下,在那里澄清了,在等待完成后继续执行可能在不同的线程上,这取决于当前的SynchronizationContext。幸运的是,对于UI线程,上下文确保返回到该UI线程。因此,对于UI来说,它肯定是一个线程。[我的意思是,如果执行await,但不要执行Task.Run]。 - ToolmakerSteve
3个回答

32
  1. 创建异步方法的目的是为了以后可以等待它们。有点像“我要把水烧开,然后整理其他汤料,最后回到锅里等待水烧开,以便做晚饭。”您开始烧水,它会异步地烧开,同时您可以做其他事情,但最终您必须停下来等待它。如果您想要“发射并忘记”,那么Async和Await是不必要的。

在C#中执行“发射并忘记”方法的最简单方法是什么?

  1. 启动新任务将该任务排队以在线程池线程上执行。线程在进程的上下文中执行(例如运行应用程序的可执行文件)。如果这是在IIS下运行的Web应用程序,则该线程在IIS工作进程的上下文中创建。该线程与主执行线程分开执行,因此无论您的主执行线程正在做什么,它都会继续自己的工作,同时您的主执行线程继续进行自己的工作。

1
感谢您的回答。 使用“await Task.Run((() =>“Do Work”));”的最大原因之一是我想启动一个任务来执行某些操作,并且在继续下一行代码之前等待它完成。我这样做对吗? 那么,当执行“await Task.Run((() =>“Do Work”));”时,它只会启动1个线程还是会启动2个线程? - syncis
1
那是正确的。 await 意味着“在此停止执行,直到我的异步任务完成”,并阻塞调用线程,直到异步线程完成。我已经在现实生活中使用过它,在应用程序启动时缓存来自 Web 服务的数据。应用程序会向不同的服务发出 5 个异步调用,然后等待它们全部返回,然后才继续执行。这使得所有调用可以同时发生,但仍然阻塞到全部完成。无论您使用异步/等待还是只是 Task.Run,任务都会在单独的线程池线程上启动。 - DVK
1
我能理解的最简单的答案,非常感谢。 - syncis
13
await 不会阻塞当前线程。 async/await 关键字与并发无关。await 仅仅检查任务是否已完成。如果完成,程序执行继续进行;如果未完成,则方法将返回一个未完成的任务给调用堆栈中上一层的方法。异步/等待的一个要点就是在等待异步操作完成时不必阻塞当前线程。 - sara
10
@DVK 我是在指你的评论,你声称 await 阻塞了当前线程,暗示 async/await 与线程有任何关系,甚至声称 await 会生成线程等等。所有这些都是事实上错误的。 - sara
显示剩余3条评论

18

1

如果您不使用 await 等待 Task, 或者使用 await 等待它,会有很大的区别:

  • 情况不使用 await: 调用了 DoSomething,但在 DoSomethingTask 完成之前,下一句话就被执行了。

  • 情况使用 await: 调用了 DoSomething,并且在 DoSomethingTask 完成后,才会执行下一句话。

因此,是否需要使用 async/await 取决于您如何调用 DoSomething:如果您不使用 await,那就像以"点火并忘记"的方式调用它。

2

这个方法是在主 UI 上运行但在一个单独的线程上,还是它在一个单独的线程上,并且在该方法内部是异步地进行的?

异步代码有时意味着其他线程(参见此 Q&A:异步 vs 多线程 - 有什么区别?)。也就是说,无论这段代码是在与 UI 线程不同的线程中执行还是在让 UI 线程继续处理而恢复执行时,它都可以使 UI 循环继续更新屏幕而其他任务在并行执行时不会冻结 UI,这很好。

异步方法(即 async 方法)是告诉编译器 await 语句应该被当作状态机来对待的一个语法糖。C# 编译器将您的 async/await 代码转换为状态机,其中等待 Task 结果的代码在被等待的代码之后执行。

有趣的 Q&A

您可能想查看以下其他 Q&A:

OP 说...

[...] 但这是否意味着 "async/await" 会出现一个线程,而 Task.Run 也会出现一个线程,或者它们都是同一个线程?

使用 async-await 并不意味着 "我创建了一个线程"。它只是一种优雅地实现连续性的语法糖。一个任务可能是一个线程,也可能不是。例如,Task.FromResult(true)public Task<bool> SomeAsync() { // This way, this method either decides if its code is asynchronous or // synchronous, but the caller can await it anyway! return Task.FromResult(true); }


4
async/await从不创建新线程——它会在调用它的同一线程上执行续体。无论所调用的可等待方法是否在新线程上执行,async/await都毫不在意。 - markmnl
3
我现在明白了,这并不取决于当前的上下文,我改正了以前的想法 :) https://dev59.com/rWEh5IYBdhLWcg3w9Xan - markmnl
2
在ASP.NET WebAPI/MVC Core中,await恢复到另一个线程池线程的事实是一个巨大的优势。尽管它在UI世界中提供了简单性(正如您之前指出的那样),但任何具有共享资源的代码默认情况下都应该是线程安全的,否则它可能在WPF中运行良好,但在ASP.NET WebAPI中会导致完全的灾难。 - Matías Fidemraizer
1
结论:我们需要将 async-await 视为永远不会在操作被暂停的同一线程上恢复工作。 - Matías Fidemraizer
1
@markmnl 顺便说一下,我的意思是你不能一概而论它会在调用者的线程上恢复。这取决于上下文/API。因此,如果有人问我这个问题,我会回答:“不,这取决于SynchronizationContext”。 - Matías Fidemraizer
显示剩余10条评论

13
类型 Task<TResult> 要求您从任务中返回一个 TResult。如果没有要返回的内容,您可以使用 Task (这是 Task<TResult> 的基类)。

但请记住,任务不是线程。 任务是要完成的工作,而线程是工人。 程序运行时,任务和工人会变得可用或不可用。在幕后,库会将您的任务分配给可用的工人,并且由于创建新工人是昂贵的操作,它通常会优先重用现有的工人,通过线程池。


2
谢谢你的回答。 但是,在线程池上启动一个新任务实际上会使用与UI线程不同的线程。我真的看不出为什么要使用async/await,除非我想触发一个任务并等待它完成,然后再执行下一条代码。:/ - syncis
@syncis,那有什么问题吗? - Voo
使用await的真正原因之一是隐藏通常需要将结果调度回特定线程(例如UI线程)所需的所有丑陋样板代码。可等待任务具有“同步上下文”。如果它是默认值,那么它将使用线程池安排继续执行 - 任何线程都可以。但是,如果您与例如WPF相关联的同步上下文,并且从UI线程await,则继续执行将自动分派回UI线程。void button_Click(..) { UIThreadStuff(); await WorkerThreadStuffAsync(); MoreUIThreadStuff(); } - Jonathan Gilbert
当我们写await funconeAsync()await Task.Run(funcone)时,funcone会在UI线程中运行还是在单独的线程中运行? - variable
当您使用await关键字等待异步方法时,它将同步运行直到第一个await。 因此,这取决于在funconeAsync()的第一个await之前是否有代码。 如果在第一个await之前有代码,则await funconeAsync()将导致funconeAsync()同步运行,然后在不同的线程(而非UI线程)上运行在funconeAsync()内等待的方法,但是如果没有使用ConfigureAwait(false),则await funconeAsync()中的代码仅将在UI线程上运行。 await Task.Run(funcone)将导致funcone内部的任何代码不会在UI上运行。 - David Klempfner

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