JavaScript与C#中的 Async/Await差异

11
我正在尝试理解异步编程,并了解了async/await关键字。我卡在了理解async/await关键字的使用上。我实际上查看了两种编程语言JavaScript和C#,发现它们在使用async/await关键字方面存在很大的差异。
对于JavaScript,它说:
Async/await使您的代码看起来同步,并在某种程度上使其表现得更加同步。await关键字阻止执行所有跟随它的代码,直到Promise兑现,就像同步操作一样。
链接:https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await#:~:text=Async%2Fawait%20makes%20your%20code,would%20with%20a%20synchronous%20operation
因此,它表示async/await将使执行同步进行。
对于C#,它说:
async关键字将方法转换为一个异步方法,允许您在其主体中使用await关键字。当应用await关键字时,它会挂起调用方法并将控制权返回给其调用者,直到等待的任务完成。
链接:https://learn.microsoft.com/en-us/dotnet/csharp/async#:~:text=The%20async%20keyword%20turns%20a,used%20inside%20an%20async%20method
因此,它表示使用async/await将使代码执行异步操作。
我想问一下,在JavaScript和C#中使用async/await关键字是否真的有区别?
或者,
上述说明是否有遗漏之处?

7
它的意思是async/await会使执行变成同步吗?不是这个意思。它的意思是它会使代码“看起来”像同步执行。 - Robert Harvey
2
如果你仔细阅读,就可以发现它们都在说同一件事情。"await关键字会阻止其后所有代码的执行,直到该Promise被兑现"和"When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete."这两句话是完全一样的表述。 - Robert Harvey
1
await 不会阻塞线程。在此期间,其他操作仍然可以在其他地方进行(例如 setInterval)。然而,在 await 下面的代码行将在 await 完成后执行。它不会使异步操作同步化,只是一些解决 Promise 回调地狱的神奇语法糖。 - Jeremy Thille
async/await在C#和JavaScript中的行为类似。实际上,C#的Task和JavaScript的Promise在思想上是相似的。它们在如何利用资源(线程)进行async/await操作方面存在一些差异(因为C#是多线程的,而JS是单线程的),以及其他差异,例如对操作的控制(例如延迟启动、停止等)。然而,从编码/工程角度来看,它们提供了相同的体验。 - Yeldar Kurmangaliyev
在JavaScript中,它说,“await关键字阻止其后面的所有代码执行,直到承诺实现,就像同步操作一样”。相比之下,在C#中,它说,“应用await关键字时,它会暂停调用方法并将控制权返回给其调用者,直到等待的任务完成”。 - Hem Bhagat
2个回答

12
Javascript文档说,“它使你的代码看起来像同步的”,而不是说“它使你的代码同步化”。
在行为上,javascript和c#在async/await方面没有区别。
async关键字表示此方法中存在异步操作。 await关键字有助于将CPS(continuation passing style)编码转换为看起来像同步的代码。 CPS类似于在promise后使用then()或在C# Tasks中使用ContinueWith()。
在“await”之前的任何代码都在当前线程下同步运行。当执行到达“await”时,等待的操作会在一个新线程下开始(不一定是新线程,但假设是新线程),因此异步操作开始,并且当前线程被释放。当等待的操作结束时,执行返回到“await”关键字所在的位置,继续执行。
javascript和C#的内部工作方式不同。
Javascript是事件驱动的。 Javascript中有一个主事件循环和一个单线程。当等待的操作完成时,在幕后会引发一个事件,主单线程继续执行异步函数的执行位置。
在C#中,没有事件循环或单个线程。我们要么使用手动线程并在它们完成工作后显式等待和加入它们,要么使用类似于async/await的东西代表我们管理线程和继续管理。使用TPL,C#'s async/await内部使用回调将异步代码的连续性传递给另一个任务。
无论如何,“await”关键字将复杂的嵌套then() -js-或ContinueWith() - c#-链转换为一个简单美丽的代码,看起来像一个普通的-同步的-代码。
function doSomethingCPS() {
   asyncOperation1()
      .then(r1 => { consume(r1); return asyncOperation2() })
      .then(r2 => { consume(r2); return asyncOperation3() })
      .then(r3 => { consume(r3); })
}

async function doSomethingAsync() {
   var r1 = await asyncOperation1();
   consume(r1);
   var r2 = await asyncOperation2();
   consume(r2);
   var r3 = await asyncOperation3();
   consume(r3);
}

这两段代码是等价的。但是,后者更加简单易读。

Javascript和C#中的线程管理有所不同。

正如所说,在Javascript中只有一个线程。如果它由于任何原因被阻塞,页面将被阻塞。这就是浏览器在20-30秒后显示“页面未响应”或“此页面中存在阻止页面的Javascript代码”消息的时候。

在HTML5中引入了Worker线程,可以实现真正的单独线程。然而,这是另一个话题。

你可能会问,如果Javascript中只有一个线程,异步操作怎么可能在另一个线程下运行呢?!

好问题。在Javascript中,尽管只有一个单一线程,但有些对象本身就使用独立的线程。定时器 - setInterval() 和 setTimeout() - XMLHttpObject() 和 fetch() 是其中的好例子。因此,在Javascript中我们确实可以有异步代码。

最后一个点是C#使用线程的方式。在C#中,async/await是使用名为TPL(Task Parallel Library)的库工作的。在TPL的核心有一个名为Task的类,类似于异步任务。

我们需要知道的真正要点是,一个Task等同于一个异步操作,但并不意味着一个Task显式地使用单独的线程。TPL中有一个任务调度程序来控制内部线程的使用。如果Task的工作很快,使用单独的线程会浪费资源,所以该任务将在当前线程下运行。

我们唯一需要知道的是,Task是异步代码的逻辑单元。事实上,Task是为了解放我们手动管理线程而引入的,这几乎是低级别的代码。

使用async/await,我们不再需要所有的样板代码,以提供异步代码。我们可以专注于我们的业务代码。


太棒了!我找到了这篇介绍C#和JS异步等待之间相似性和差异深入的中等文章:https://mattmazzola.medium.com/comparing-asynchronous-patterns-between-c-and-javascript-2137793d7e37 - Sudhanshu Mishra

4

我对JavaScript不是很熟悉,但这个语句的意思是:

异步/等待使您的代码看起来像同步,并在某种程度上使其行为更加同步。等待关键字阻止所有接下来的代码执行,直到承诺实现,就像同步操作一样。

这与C#异步/等待非常相似。在两种情况下,您的代码看起来像同步执行,您的执行顺序是连续的。因为在C#中,当您的代码如下所示时:

// ...
await FooAsync();
Console.WriteLine("Await has returned the execution");

看起来似乎您的执行线程正在运行FooAsync,然后同一线程正在调用Console.WriteLine。但实际上,当执行线程命中await时,它会在幕后执行很多操作。这里有一篇好文章。但在大多数情况下,

当应用await关键字时,它将挂起调用的方法,并将控制权交回给其调用者,直到等待的任务完成为止。

执行您的代码的线程将继续进行其他操作。然后,由另一个(或相同的)线程在FooAsync完成后继续执行Console.WriteLine。 这种行为对于像WPF或WinForms应用程序中的UI线程非常有帮助。 例如,FooAsync是一个非常耗费时间的方法。它进行了大量计算,并且需要很长时间才能完成。但是您正在UI上运行您的代码,并且当用户点击按钮时,底层代码由UI线程执行。因此,如果您按照以下方式同步运行和等待FooAsync

FooAsync().Result;

如果用户界面被“冻结”,用户将会失望。因此,当您执行操作时

await FooAsync();

UI线程“请求”TaskScheduler通过任何可用的线程运行FooAsync。任务完成后,TaskScheduler尝试执行下一条语句:

Console.WriteLine("Await has returned the execution");

再次由UI线程处理,就像同步操作一样。

与同步操作完全相同。


1
@variable 答案是 - 这取决于情况。Async/await 会生成一个状态机,该状态机可能会使用单个线程或多个线程执行,这取决于所等待的任务。如果任务在等待时已经完成,则不会使用额外的线程。否则 - 任务调度程序将决定,但通常涉及多个线程。我建议阅读 Jeffrey Richter 的《CLR via C#》。该过程在书中有详细描述。 - Gleb

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