ChatGPT解决这个技术问题 Extra ChatGPT

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

我认为它们基本上是一样的——编写在处理器之间分割任务的程序(在具有 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 关键字表示“无论何时调用此函数,都不会在调用后的所有内容都需要完成的上下文中调用此函数。”

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

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 感到困惑。)

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

R
Robert Harvey

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

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

同步:你先煮鸡蛋,然后再煮吐司。

异步、单线程:您开始煮鸡蛋并设置计时器。你开始烤面包,并设置一个计时器。当他们都在做饭的时候,你打扫厨房。当计时器响起时,您将鸡蛋从火中取出,将吐司从烤面包机中取出并上桌。

异步、多线程:您再雇用两名厨师,一名煮鸡蛋,一名煮吐司。现在你有协调厨师的问题,以便他们在共享资源时不会在厨房中相互冲突。你必须付钱给他们。

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

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

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

有人调用 DisplayWebSiteLength。谁?我们不在乎。

它设置一个标签,创建一个客户端,并要求客户端获取一些东西。客户端返回一个对象,表示获取某物的任务。该任务正在进行中。

它在另一个线程上进行吗?可能不是。阅读斯蒂芬关于为什么没有线程的文章。

现在我们等待任务。怎么了?我们检查任务是否在我们创建它和等待它之间完成。如果是,那么我们获取结果并继续运行。假设它还没有完成。我们将该方法的其余部分注册为该任务的继续并返回。

现在控制权已返回给调用者。它有什么作用?不管它想要什么。

现在假设任务完成。它是怎么做到的?也许它正在另一个线程上运行,或者我们刚刚返回的调用者允许它在当前线程上运行完成。无论如何,我们现在已经完成了一项任务。

完成的任务要求正确的线程——同样,可能是唯一的线程——来运行任务的延续。

控制立即返回到我们刚刚在等待点离开的方法。现在有一个可用的结果,因此我们可以分配文本并运行该方法的其余部分。

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


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

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

您不必担心多段代码同时接触相同的对象:每个函数都会在允许在页面上运行任何其他 javascript 之前完成运行。 (更新:自从编写此代码以来,JavaScript 添加了 async functionsgenerator functions。这些函数并不总是在执行任何其他 javascript 之前运行完成:每当它们到达 yieldawait 关键字时,它们让其他 javascript 执行,并且可以在以后继续执行,类似于 C# 的 async 方法。

但是,当执行 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 Worker API 本身是异步的,并且不允许 Web Worker 直接影响它们被调用的网页上的 javascript 值或 DOM from,这意味着该答案的关键第二段仍然成立。从程序员的角度来看,调用 Web Worker 和调用 AJAX 请求之间几乎没有区别。
浏览器内 Javascript 是没有线程的异步程序的一个很好的例子 有点迂腐 - 总是至少有 1 个执行线程
@KejsiStruga:大声笑,点了。将“无线程”更改为“无多线程”