异步编程和多线程有什么区别?


234

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

异步方法旨在作为非阻塞操作。在等待任务运行时,异步方法中的等待表达式不会阻塞当前线程。取而代之的是,表达式将方法的其余部分作为继续进行签名,并将控制权返回给异步方法的调用者。

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

我想知道是否有人可以帮我翻译成英文。似乎在异步性(是一个词?)和线程之间进行了区分,这意味着您可以拥有一个具有异步任务但没有多线程的程序。

现在,我了解了异步任务的想法,例如pg上的示例。乔恩·斯基特(Jon Skeet)的《C#深度》第 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关键字的意思是“ 这个功能,无论何时它被调用时,不会在这是需要的一切它的完成被称为它的呼叫后,上下文调用。”

换句话说,将其写在某些任务的中间

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

,由于DisplayWebsiteLength()x或无关y,将导致DisplayWebsiteLength()“在后台”执行,例如

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

显然,这是一个愚蠢的例子,但是我是正确的,还是我完全感到困惑?

(此外,我对于上面的函数为什么sender以及e从未使用过它感到困惑。)



sendere暗示这实际上是一个事件处理程序-几乎是唯一需要的地方async void。最有可能的是,这是在单击按钮或类似操作时调用的-结果是此操作相对于应用程序的其余部分完全异步发生。但是它仍然全部集中在一个线程上-UI线程(在将回调发送到UI线程的IOCP线程上只有很少的时间)。
a安


3
关于DisplayWebsiteLength代码示例的一个非常重要的注意事项:请勿HttpClientusing语句中使用-在繁重的负载下,代码可能耗尽可用的套接字数量,从而导致SocketException错误。有关不正确的实例化的更多信息。

1
@JakubLortz我真的不知道这篇文章是针对谁的。不适合初学者,因为它需要有关线程,中断,CPU相关内容等方面的丰富知识。不适合高级用户,因为对于他们来说,这一切已经很清楚了。我确信它不会帮助任何人理解它的全部含义-太高的抽象水平。
洛雷诺

Answers:


589

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

类比通常会有所帮助。您在餐厅做饭。订购鸡蛋和吐司。

  • 同步:先煮鸡蛋,再煮吐司。
  • 异步单线程:您开始煮鸡蛋并设置计时器。您开始烤面包,并设置一个计时器。他们俩都在做饭时,您要打扫厨房。当计时器关闭时,您将鸡蛋和干面包从烤面包机中取出并送达。
  • 异步,多线程:您再雇用两名厨师,一名厨师煮鸡蛋,一名厨师烤面包。现在您需要协调厨师,以便他们在共享资源时不会在厨房中互相冲突。而且你必须付钱。

现在,多线程仅仅是一种异步有意义吗? 线程是关于工人的。异步是关于任务的。在多线程工作流程中,您将任务分配给工作人员。在异步单线程工作流中,您可以看到一个任务图,其中某些任务取决于其他任务的结果。在每个任务完成时,给定刚完成的任务的结果,它将调用计划下一个可以运行的任务的代码。但是,您(希望)仅需要一名工作人员即可执行所有任务,而不需要每个任务一名工作人员。

这将有助于认识到许多任务不是处理器约束的。对于与处理器相关的任务,合理的做法是雇用与处理器数量一样多的工作程序(线程),为每个工作程序分配一个任务,为每个工作程序分配一个处理器,并且让每个处理器除了计算结果外,别无其他工作尽快。但是对于没有在处理器上等待的任务,您根本不需要分配工作器。您只需要等待消息到达就可以得到结果,然后在等待时执行其他操作即可。当该消息到达时,您可以将已完成任务的继续安排为待办事项列表上的下一件事以进行核对。

因此,让我们更详细地看一下乔恩的例子。怎么了?

  • 有人调用DisplayWebSiteLength。WHO?我们不在乎。
  • 它设置一个标签,创建一个客户端,并要求客户端获取一些东西。客户端返回一个代表获取某些东西的任务的对象。该任务正在进行中。
  • 是否在另一个线程上进行?可能不是。阅读Stephen的文章,了解为什么没有线程。
  • 现在我们等待任务。怎么了?我们检查任务在创建和等待之间是否完成。如果是,那么我们获取结果并继续运行。让我们假设它还没有完成。 我们将该方法的其余部分签名为该任务的继续并返回
  • 现在,控制权已返回给调用者。它有什么作用?无论它想要什么。
  • 现在,假设任务已完成。它是怎么做到的?也许它正在另一个线程上运行,或者也许我们刚刚返回的调用方允许它在当前线程上运行完成。无论如何,我们现在有一个完成的任务。
  • 完成的任务要求正确的线程(可能是唯一的线程)再次运行任务。
  • 控制权立即返回到我们在等待时刚离开的方法中。现在有一个结果,以便我们可以分配text和运行方法的其余部分。

就像我的类比。有人要求您提供文件。您发送了该文档的邮件,并继续进行其他工作。当它到达邮件中时,您会收到信号通知,并且当您感到喜欢时,就完成了其余的工作流程-打开信封,支付邮寄费,无论如何。您无需雇用其他工人即可为您完成所有这些工作。


8
@ user5648283:硬件是考虑任务的错误级别。任务只是一个对象,它(1)表示某个值将来会变得可用,并且(2)当该值可用时可以运行代码(在正确的线程上)。将来任何单个任务如何获得结果都取决于它。有些人会使用“磁盘”和“网卡”之类的特殊硬件;有些会使用CPU等硬件。
埃里克·利珀特

13
@ user5648283:再想一想我的比喻。当有人要您煮鸡蛋和烤面包时,您可以使用特殊的硬件-火炉和烤面包机-并且您可以在硬件工作时清理厨房。如果有人要求您提供鸡蛋,吐司和对最后一部霍比特人电影的原始评论,则可以在鸡蛋和吐司做饭时写下您的评论,但您无需为此使用硬件。
埃里克·利珀特

9
@ user5648283:现在,关于“重新排列代码”的问题,请考虑一下。假设您有一个方法P,该方法具有收益率收益,而方法Q对每个P的结果进行求值。单步执行代码。您会看到我们先运行Q一点,然后运行P一点,然后运行Q一点...您了解这一点吗? 等待本质上是化装的收益。现在更清楚了吗?
埃里克·利珀特

10
烤面包机是硬件。硬件不需要线程来为其服务;磁盘和网卡的运行水平远低于操作系统线程的水平。
埃里克·利珀特

5
@ShivprasadKoirala:绝对不是真的。如果您相信这一点,那么您会对异步有一些非常错误的信念。在C#异步的全部意义在于,它并没有创建一个线程。
埃里克·利珀特

27

浏览器内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

6
@oleksii:从技术上讲这是正确的,但是我不打算讲这个,因为Web Workers API本身是异步的,并且不允许Web Workers直接影响它们被调用的网页上的javascript值或DOM。从,这意味着此答案的关键第二段仍然成立。从程序员的角度来看,调用Web Worker和调用AJAX请求之间没有什么区别。
StriplingWarrior
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.