异步编程和多线程有啥区别?

Posted

技术标签:

【中文标题】异步编程和多线程有啥区别?【英文标题】:What is the difference between asynchronous programming and multithreading?异步编程和多线程有什么区别? 【发布时间】:2016-04-13 08:50:31 【问题描述】:

我认为它们基本上是一样的——编写在处理器之间分割任务的程序(在具有 2 个以上处理器的机器上)。然后我正在阅读this,上面写着:

异步方法旨在成为非阻塞操作。一个等待 异步方法中的表达式不会阻塞当前线程,而 等待的任务正在运行。相反,该表达式签署了其余部分 方法的延续并将控制权返回给调用者 异步方法。

async 和 await 关键字不会导致额外的线程 创建的。异步方法不需要多线程,因为异步 方法不在自己的线程上运行。该方法在当前运行 同步上下文并仅在线程上使用时间 方法处于活动状态。您可以使用 Task.Run 将 CPU 密集型工作移动到 后台线程,但后台线程对进程没有帮助 那只是在等待结果可用。

我想知道是否有人可以为我将其翻译成英文。它似乎区分了异步性(这是一个词吗?)和线程,并暗示您可以拥有一个具有异步任务但没有多线程的程序。

现在我了解了异步任务的概念,例如 pg 上的示例。 Jon Skeet 的 C# In Depth,第三版 467

async void DisplayWebsiteLength ( object sender, EventArgs e )

    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    

async 关键字的意思是“此函数,无论何时被调用,都不会在调用后的所有内容都需要完成的上下文中调用。” p>

换句话说,在某个任务的中间写它

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

,由于DisplayWebsiteLength()xy无关,将导致DisplayWebsiteLength()“在后台”执行,比如

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

显然这是一个愚蠢的例子,但我是正确的还是我完全糊涂了还是什么?

(另外,我很困惑为什么 sendere 从未在上述函数的主体中使用。)

【问题讨论】:

这是一个很好的解释:blog.stephencleary.com/2013/11/there-is-no-thread.html sendere 暗示这实际上是一个事件处理程序——几乎是唯一需要async void 的地方。最有可能的是,这会在按钮单击或类似的情况下调用 - 结果是此操作相对于应用程序的其余部分完全异步发生。但它仍然都在一个线程上 - UI 线程(在将回调发布到 UI 线程的 IOCP 线程上有一小段时间)。 Difference between Multithreading and Async program in c#的可能重复 关于DisplayWebsiteLength 代码示例的一个非常重要的说明:您不应该在using 语句中使用HttpClient - 在重负载下,代码可能会耗尽可用套接字的数量,从而导致SocketException 错误。有关Improper Instantiation 的更多信息。 @JakubLortz 我不知道这篇文章是写给谁的。不适合初学者,因为它需要对线程、中断、CPU 相关的东西等有很好的了解。不适合高级用户,因为他们已经很清楚了。我相信它不会帮助任何人理解它的全部含义 - 抽象级别太高。 【参考方案1】:

您的误解非常普遍。很多人都被教导说多线程和异步是一回事,但事实并非如此。

类比通常会有所帮助。你在一家餐馆做饭。一份鸡蛋和烤面包的订单进来了。

同步:先煮鸡蛋,然后再煮吐司。 异步、单线程:您开始煮鸡蛋并设置计时器。你开始烤面包,并设置一个计时器。当他们都在做饭的时候,你打扫厨房。当计时器响起时,您将鸡蛋从火中取出,将吐司从烤面包机中取出并上桌。 异步、多线程:您再雇用两名厨师,一名煮鸡蛋,一名煮吐司。现在你有协调厨师的问题,以便他们在共享资源时不会在厨房中相互冲突。你必须付钱给他们。

现在多线程只是一种异步是否有意义? 线程是关于工人的;异步是关于任务的。在多线程工作流中,您将任务分配给工作人员。在异步单线程工作流中,您有一个任务图,其中一些任务依赖于其他任务的结果;当每个任务完成时,它会调用代码来安排下一个可以运行的任务,给定刚刚完成的任务的结果。但是您(希望)只需要一名工人来执行所有任务,而不是每个任务一名工人。

这将有助于认识到许多任务不受处理器限制。对于处理器绑定的任务,雇用与处理器一样多的工作人员(线程)是有意义的,为每个工作人员分配一个任务,为每个工作人员分配一个处理器,并且让每个处理器只做其他工作,只计算结果为尽快。但是对于不在处理器上等待的任务,您根本不需要分配工作人员。您只需等待结果可用的消息到达,然后在等待期间执行其他操作。当该消息到达时,您可以安排已完成任务的继续,作为待办事项列表中的下一个要检查的事情。

让我们更详细地看一下 Jon 的示例。发生什么了?

有人调用 DisplayWebSiteLength。谁?我们不在乎。 它设置一个标签,创建一个客户端,并要求客户端获取一些东西。客户端返回一个对象,表示获取某物的任务。该任务正在进行中。 它是否在另一个线程上进行?可能不是。阅读Stephen's article 了解为什么没有线程。 现在我们等待任务。发生什么了?我们检查任务是否在我们创建它和等待它之间完成。如果是,那么我们获取结果并继续运行。假设它还没有完成。 我们将该方法的其余部分注册为该任务的继续并返回。 现在控制权已返回给调用者。它有什么作用?随心所欲。 现在假设任务完成。它是怎么做到的?也许它正在另一个线程上运行,或者我们刚刚返回的调用者允许它在当前线程上运行完成。无论如何,我们现在已经完成了一项任务。 已完成的任务要求正确的线程——同样,可能是only线程——来运行任务的延续。 控制权立即返回到我们刚刚在等待点离开的方法。现在有一个可用的结果,因此我们可以分配text 并运行该方法的其余部分。

就像我的比喻一样。有人向你要文件。你把文件寄出去,然后继续做其他工作。当它到达邮件时,您会收到信号,当您愿意时,您会完成其余的工作流程——打开信封,支付运费,等等。您无需雇用其他工人为您完成所有这些工作。

【讨论】:

@user5648283:硬件是考虑任务的错误级别。 任务只是一个对象,它 (1) 表示一个值将在未来变得可用,并且 (2) 当该值可用时可以运行代码(在正确的线程上)。任何单个任务将来如何获得结果都取决于它。有些人会使用“磁盘”和“网卡”等特殊硬件来做到这一点;有些会使用 CPU 等硬件。 @user5648283:再想想我的比喻。当有人要求你煮鸡蛋和烤面包时,你会使用特殊的硬件——炉子和烤面包机——你可以在硬件工作的时候打扫厨房。如果有人向您要鸡蛋、吐司和对上一部霍比特人电影的原创评论,您可以在鸡蛋和吐司正在烹饪时写评论,但您不需要为此使用硬件。 @user5648283:现在关于“重新排列代码”的问题,请考虑一下。假设你有一个方法 P,它有一个 yield return,还有一个方法 Q,它对 P 的结果进行 foreach。单步执行代码。你会看到我们运行一点点 Q 然后一点点 P 然后一点点 Q...你明白这点吗? await 本质上是化装的收益回报。现在更清楚了吗? 烤面包机是硬件。硬件不需要线程来服务它;磁盘和网卡之类的运行水平远低于操作系统线程。 @ShivprasadKoirala:这绝对不是真的。如果你相信这一点,那么你就有一些关于异步的非常错误的信念。 C# 中异步的全部意义在于它创建线程。【参考方案2】:

浏览器内 javascript 是没有多线程的异步程序的一个很好的例子。

您不必担心多段代码同时触及同一个对象:每个函数都会在允许在页面上运行任何其他 javascript 之前完成运行。

但是,在执行 AJAX 请求之类的操作时,根本没有代码在运行,因此其他 javascript 可以响应单击事件之类的操作,直到该请求返回并调用与其关联的回调。如果 AJAX 请求返回时这些其他事件处理程序之一仍在运行,则在完成之前不会调用其处理程序。只有一个 JavaScript“线程”在运行,即使您可以有效地暂停正在执行的操作,直到获得所需信息为止。

在 C# 应用程序中,任何时候处理 UI 元素时都会发生同样的事情——只有在 UI 线程上才允许与 UI 元素交互。如果用户单击了一个按钮,而您想通过从磁盘读取一个大文件来做出响应,那么没有经验的程序员可能会在单击事件处理程序本身中读取文件,这将导致应用程序“冻结”,直到文件已完成加载,因为在释放该线程之前,不允许它响应任何更多的单击、悬停或任何其他与 UI 相关的事件。

程序员可以用来避免这个问题的一个选项是创建一个新线程来加载文件,然后告诉该线程的代码,当文件被加载时,它需要再次在 UI 线程上运行剩余的代码,以便它可以根据在文件中找到的内容更新 UI 元素。直到最近,这种方法还很流行,因为 C# 库和语言让这种方法变得简单,但从根本上说它比它必须的要复杂。

如果您考虑一下 CPU 在硬件和操作系统级别读取文件时正在做什么,它基本上是在发出一条指令,将数据从磁盘读取到内存中,然后使用读取完成时的“中断”。换句话说,从磁盘读取(或任何 I/O)本质上是一个 异步 操作。等待该 I/O 完成的线程的概念是库开发人员创建的抽象概念,以使其更易于编程。没必要。

现在,.NET 中的大多数 I/O 操作都有一个可以调用的相应 ...Async() 方法,它几乎立即返回一个 Task。您可以向此Task 添加回调,以指定您希望在异步操作完成时运行的代码。您还可以指定您希望该代码在哪个线程上运行,并且您可以提供一个令牌,异步操作可以不时检查该令牌以查看您是否决定取消异步任务,使其有机会快速停止其工作优雅地。

在添加 async/await 关键字之前,C# 对如何调用回调代码的方式更加明显,因为这些回调采用与任务相关联的委托的形式。为了仍然为您提供使用 ...Async() 操作的好处,同时避免代码复杂性,async/await 抽象了这些委托的创建。但它们仍然存在于编译后的代码中。

因此,您可以让您的 UI 事件处理程序 await 进行 I/O 操作,释放 UI 线程来做其他事情,并在您完成读取文件后或多或少地自动返回到 UI 线程-- 无需创建新线程。

【讨论】:

只有一个 JavaScript“线程”在运行 - Web Workers 不再适用。 @oleksii:这在技术上是正确的,但我不打算深入讨论,因为 Web Workers API 本身是异步的,并且不允许 Web Workers 直接影响 javascript 值或 DOM在调用它们的网页上,这意味着该答案的关键第二段仍然成立。从程序员的角度来看,调用 Web Worker 和调用 AJAX 请求之间几乎没有区别。 浏览器内 Javascript 是没有线程的异步程序的一个很好的例子有点迂腐 - 总是至少有 1 个执行线程 @KejsiStruga:大声笑,点了。将“无线程”更改为“无多线程”

以上是关于异步编程和多线程有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

c++ 多线程与c多线程有啥区别?

java同步异步和多线程编程

Async await 异步编程说明

C# Winform 多线程异步委托进度条

同步和异步编程有啥区别(在node.js中)

线程也疯狂-----异步编程