Node.js与.NET中的Async/await相比如何?

56

有人能给我解释/指引一下,Node.js的异步模型(非阻塞线程)与其他语言例如C#的异步处理I/O的方式有什么区别吗?在我看来,这两种模型都是相同的。请给予建议。


1
await 帮助你 消费 异步操作,而不是编写大量的回调函数。 - SLaks
4个回答

94

这两个模型非常相似。有两个主要区别,其中一个(根据“即将”定义的说法)即将消失。

一个差异是Node.js是异步单线程的,而ASP.NET是异步多线程的。这意味着Node.js代码可以做出一些简化的假设,因为你的所有代码始终运行在同一个确切的线程上。所以当你的ASP.NET代码等待时,它可能会在不同的线程上恢复,并且你需要避免诸如线程本地状态之类的事情。

然而,对于ASP.NET来说,这种差异也是一种优势,因为它意味着async ASP.NET可以直接扩展到服务器的全部功能。如果你考虑一个8核的机器,那么ASP.NET可以同时处理(同步部分的)8个请求。如果你把Node.js放在一个升级过的服务器上,通常实际上会运行8个单独的Node.js实例,并添加类似nginx或一个简单的自定义负载均衡器来处理该服务器的路由请求。这也意味着如果你想要其他共享服务器资源(例如缓存),那么你还需要将它们移出进程。

另一个主要区别实际上是语言上的区别,而不是平台上的区别。JavaScript的异步支持仅限于回调和承诺,即使你使用最好的库,在处理任何非平凡的情况时,你仍然会得到非常尴尬的代码。相比之下,C# / VB中async/await的支持允许你编写非常自然的异步代码(更重要的是,可维护的异步代码)。

然而,语言差异正在消失。 JavaScript 的下一个版本将引入生成器,配合辅助库将会使在 Node.js 中使用异步代码与现在使用 async/await 一样自然。如果想要立即使用这个“即将到来”的功能,可以使用 V8 3.19 中添加的生成器,在 Node.js 0.11.2(不稳定分支)中进行了整合。使用 --harmony--harmony-generators 来明确启用生成器支持。

“...代码等待,它可能会在不同的线程上恢复,你需要避免像线程本地状态这样的问题…” 难道async/await也会在UI线程上与调用方法结束在同一线程上吗? - PKV
12
@PKV:ASP.NET 没有 UI 线程。相反,它有一个“请求上下文”,当 ASP.NET 线程池中的线程处理请求时,会进入该上下文。然后,当线程返回到线程池时,会退出请求上下文。使用 await 可以确保方法恢复到同一“请求上下文”,而不是同一“线程”。 - Stephen Cleary
1
我不同意这个说法。使用像async.js这样的库,即使处理非常复杂的情况,你也可以在JS中编写非常优雅的异步代码。 - UpTheCreek
3
不同的库有不同的技术/解决方案,具体取决于您想要做什么。复杂性在于将实际逻辑拆分为多个连续部分。这是生成器可以解决的复杂性。 - Stephen Cleary
1
@Spacemonkey:我不会说“更好”。肯定是不同的。绝对适用于高性能服务器。但编写正确的多线程代码比编写正确的单线程代码更困难。所以,(一旦JS正式支持async),我认为Node.js将会更容易编写。谁知道呢,也许最终因此而胜出。JS正在进行一些很棒(也是非常需要的)改进,包括async - Stephen Cleary
显示剩余2条评论

20
Node.js的异步模型和C#的async/await模型之间存在巨大差异。Node.js所具有的异步模型类似于C#和.Net中称为事件驱动异步模式(EAP)的旧异步模型。C#和.Net拥有三种异步模型,您可以在Asynchronous Programming Patterns中了解它们。C#中最现代的异步模型是基于任务的,并且使用C#的async和await关键字,您可以在Task-based Asynchronous Pattern中了解它。
C#的async/await关键字使异步代码线性化,并且比其他任何编程语言更好地避免了“回调地狱”。您只需要尝试一下,然后您将永远不会以其他方式编写代码。您只需编写使用异步操作的代码,而不必担心可读性,因为它看起来就像编写任何其他代码一样。
请观看这些视频:
  1. 异步编程深入探讨
  2. ASP.NET中的异步编程
  3. 理解异步和可等待任务

编辑: 由于Node.js V8 JavaScript引擎支持生成器,在ECMAScript 6草案中定义, JavaScript代码中的“回调地狱”也可以轻松避免。 它为JavaScript带来了某种形式的异步/等待


比起其他的,这样写会好很多。 - Lei Yang

6
使用nodejs时,所有请求都进入事件队列。Node的事件循环使用单个线程处理事件队列中的所有非IO工作,并将所有IO绑定工作发送到C ++线程池(使用JavaScript回调来管理异步性)。然后,C ++线程将其结果添加到事件队列中。
与ASP.NET(前两个适用于几乎允许异步IO的所有Web服务器)的区别在于:
1. ASP.NET对于每个传入请求使用不同的线程,因此您会得到上下文切换的开销。 2. .NET不强制使用async来执行IO绑定的工作,因此它不像nodejs那样惯用于使用回调的异步IO绑定API调用。 3. .NET的“await-async”在编译时添加了一个步骤以添加“回调函数”,因此您可以编写线性代码(无需回调函数传递),与nodejs相反。

网络上有很多地方描述了Node的架构,但这里有一些内容:http://johanndutoit.net/presentations/2013/02/gdg-capetown-nodejs-workshop-23-feb-2013/index.html#1


嘿,我明白你在这里的观点。所以应该这样理解,对于'n'个传入请求:ASP.Net会创建'n'个线程,而如果所有n个请求都需要I/O,Node.js也会创建'n'个线程。 - PKV
4
@PKV:正如我在博客中所描述的那样,异步I/O不需要线程。这对于Node.js和“async” ASP.NET都是正确的。 - Stephen Cleary
2
@billy: 是的,但它只用于那些应该有异步API但被忽视了且只有同步API的情况。在这种情况下,Node.js会将同步API包装在线程池线程中(当操作系统异步执行实际操作时,该线程会被阻塞)。因此,Node.js线程池是不完整API的解决方法;它通常不用于异步I/O。 - Stephen Cleary

3
在Nodejs和.NET中,异步的区别在于使用了抢占式多任务处理用户代码的方式。
.NET使用抢占式多任务处理用户代码,而Nodejs则不使用。
Nodejs使用内部线程池来处理IO请求,并使用单个线程来执行您的JS代码,包括IO回调函数。
使用抢占式多任务(.NET)的后果之一是,共享状态可能会在执行堆栈时被另一个执行堆栈更改。但在Nodejs中并非如此-没有来自异步操作的回调可以与当前正在执行的堆栈同时运行。在Javascript中不存在其他执行堆栈。异步操作的结果仅在当前执行堆栈完全退出时才可用于回调函数。因此,简单的while(true);会导致Nodejs挂起,因为在这种情况下,当前堆栈没有退出,下一个循环永远不会启动。
要了解差异,请考虑两个示例,一个用于js,一个用于net。 var p = new Promise(function(resolve) { setTimeout(resolve, 500, "my content"); }); p.then(function(value) { // ... value === "my content"
在此代码中,您可以安全地在“启动”异步操作后放置处理程序(then),因为您可以确信,在整个当前调用堆栈退出之前,由异步操作启动的任何回调代码都不会执行。回调在下一周期中处理。至于计时器回调函数,它们也是一样处理的。异步计时器事件只是将回调处理放在队列中,在下一个周期中进行处理。
在.NET中情况不同。没有周期。有抢占式多任务处理。
ThreadPool.QueueUserWorkItem((o)=>{eventSource.Fire();});
eventSource.Fired += ()=>{
 // the following line might never execute, because a parallel execution stack in a thread pool could have already been finished by the time the callback added.
 Console.WriteLine("1");
}

这里有一个Hello World .NET代码,类似于Nodejs,演示了在单个线程上进行异步处理和使用线程池进行异步IO。(.NET包括TPL和IAsyncResult版本的异步IO操作,但是在本例中没有区别。无论如何,所有操作最终都会在线程池中以不同的线程完成。)

void Main()
{
    // Initializing the test
    var filePath = Path.GetTempFileName();
    var filePath2 = Path.GetTempFileName();
    File.WriteAllText(filePath, "World");
    File.WriteAllText(filePath2, "Antipodes");

    // Simulate nodejs
    var loop = new Loop();

    // Initial method code, similar to server.js in Nodejs. 
    var fs = new FileSystem();

    fs.ReadTextFile(loop, filePath, contents=>{
        fs.WriteTextFile(loop, filePath, string.Format("Hello, {0}!", contents),
            ()=>fs.ReadTextFile(loop,filePath,Console.WriteLine));
    });

    fs.ReadTextFile(loop, filePath2, contents=>{
        fs.WriteTextFile(loop, filePath2, string.Format("Hello, {0}!", contents),
            ()=>fs.ReadTextFile(loop,filePath2,Console.WriteLine));
    });

    // The first javascript-ish cycle have finished.

    // End of a-la nodejs code, but execution have just started.

    // First IO operations could have finished already, but not processed by callbacks yet

    // Process callbacks
    loop.Process();

    // Cleanup test
    File.Delete(filePath);
    File.Delete(filePath2);
}

public class FileSystem
{
    public void ReadTextFile(Loop loop, string fileName, Action<string> callback)
    {
        loop.RegisterOperation();
        // simulate async operation with a blocking call on another thread for demo purposes only.
        ThreadPool.QueueUserWorkItem(o=>{
            Thread.Sleep(new Random().Next(1,100)); // simulate long read time

            var contents = File.ReadAllText(fileName);
            loop.MakeCallback(()=>{callback(contents);});
        });
    }

    public void WriteTextFile(Loop loop, string fileName, string contents, Action callback)
    {
        loop.RegisterOperation();
        // simulate async operation with a blocking call on another thread for demo purposes only.
        ThreadPool.QueueUserWorkItem(o=>{
            Thread.Sleep(new Random().Next(1,100)); // simulate long write time

            File.WriteAllText(fileName, contents);
            loop.MakeCallback(()=>{callback();});
        });
    }
}

public class Loop
{
    public void RegisterOperation()
    {
        Interlocked.Increment(ref Count);
    }
    public void MakeCallback(Action clientAction)
    {
        lock(sync)
        {
            ActionQueue.Enqueue(()=>{clientAction(); Interlocked.Decrement(ref Count);});
        }
    }

    public void Process()
    {
        while(Count > 0)
        {
            Action action = null;
            lock(sync)
            {
                if(ActionQueue.Count > 0)
                {
                    action = ActionQueue.Dequeue();
                }
            }

            if( action!= null )
            {
                action();
            }
            else
            {
                Thread.Sleep(10); // simple way to relax a little bit.
            }
        }
    }

    private object sync = new object();

    private Int32 Count;

    private Queue<Action> ActionQueue = new Queue<Action>();
}

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