.NET 中是不是可以使用 C# 实现基于事件的异步模式而无需多线程?

Posted

技术标签:

【中文标题】.NET 中是不是可以使用 C# 实现基于事件的异步模式而无需多线程?【英文标题】:Is it possible in .NET, using C#, to achieve event based asynchronous pattern without multithreading?.NET 中是否可以使用 C# 实现基于事件的异步模式而无需多线程? 【发布时间】:2011-02-19 23:59:14 【问题描述】:

我对@9​​87654321@ 的架构设计感到惊讶,并想知道 C# 是否能够进行这样的设计:

异步、基于事件/事件循环、非阻塞I/O 无多线程。

【问题讨论】:

BeginRead (msdn.microsoft.com/en-us/library/…) 能做到这一点吗? Windows 上的所有网络 I/O 都可以是异步的、基于事件的,并且在 16 位时代没有多线程,因为 Windows 编程是事件驱动的,而 Win16 不是多线程的。 @Gabe,请查看我对@Jeremy 回答的评论。 这个问题与C#无关。 C# 是一种没有 IO 或线程的编程语言。 @John:我认为将它与 C# 相关联是相关的 - 有一些 C# 特性可以实现更有趣的异步模型(例如 yield return 和 CCR)。 【参考方案1】:

我认为所有实现标准异步编程模型的BeginXyz操作都在线程池线程上运行回调,这使得应用程序自动多线程。

但是,您可以通过使用 Control.Invoke 或更一般的 SynchronizationContext 为 Windows 应用程序维护的单个 GUI 线程同步所有操作来实现单线程异步编程模型。

BeginXyz 的每次调用都必须按照以下方式重写:

// Start asynchronous operation here (1)
var originalContext = SynchronizationContext.Current;
obj.BeginFoo(ar =>
  // Switch to the original thread
  originalContext.Post(ignored => 
    var res = obj.EndFoo(); 
    // Continue here (2)
  ));

标记为 (2) 的代码将继续在与 (1) 中的代码相同的线程上运行,因此您将仅使用线程池线程将回发转发回原始(单个)线程。

附带说明,F# 中的异步工作流更直接地支持这一点,它可用于相当优雅的 GUI 编程风格as described here。我不知道node.js,但我想您可能也会对 F# 异步工作流感到惊讶,因为它们对于异步/基于事件/...的编程风格真的很酷 :-)

【讨论】:

【参考方案2】:

我正在 .NET 中做这样的事情,作为一个宠物项目。我叫它ALE (Another Looping Event)...因为啤酒。

现在是非常 alpha,但都是自制的,因为和你一样,我想知道它是否可以完成。

这是一个事件循环架构。 它利用 .NET 的非阻塞异步 I/O 调用 它使用回调样式调用来帮助开发人员编写更具可读性的异步代码。 异步网络套接字实现 异步 http 服务器实现 异步 sql 客户端实现

与其他一些尝试不同,我发现它不仅仅是一个带有事件循环的 Web 服务器。你可以写出任何类型的基于事件循环的应用程序。


示例

以下代码将:

启动事件循环 在端口 1337 上启动 Web 服务器 在端口 1338 上启动 Web 套接字服务器 然后读取一个文件并用它“DoSomething”:
EventLoop.Start(() => 

    //create a web server
    Server.Create((req, res) => 
        res.Write("<h1>Hello World</h1>");
    ).Listen("http://*:1337");


    //start a web socket server
    Net.CreateServer((socket) => 
        socket.Receive((text) => 
             socket.Send("Echo: " + text);
        );
    ).Listen("127.0.0.1", 1338, "http://origin.com");


    //Read a file
    File.ReadAllText(@"C:\Foo.txt", (text) => 
        DoSomething(text);
    );
);

所以我想我的答案是“是的”,它可以用 C#... 或几乎任何语言来完成。真正的诀窍是能够利用本机非阻塞 I/O。

有关该项目的更多信息将发布here。

【讨论】:

【参考方案3】:

当然,它只需要一个事件循环。比如:

class EventLoop 
   List<Action> MyThingsToDo  get; set; 

   public void WillYouDo(Action thing) 
      this.MyThingsToDo.Add(thing);
   

   public void Start(Action yourThing) 
      while (true) 
         Do(yourThing);

         foreach (var myThing in this.MyThingsToDo) 
            Do(myThing);
         
         this.MyThingsToDo.Clear();
      
   

   void Do(Action thing)  
      thing();
   


class Program 
    static readonly EventLoop e = new EventLoop();

    static void Main() 
        e.Start(DoSomething);
    

    static int i = 0;
    static void DoSomething() 
        Console.WriteLine("Doing something...");
        e.WillYouDo(() => 
            results += (i++).ToString();
        );
        Console.WriteLine(results);
    

    static string results = "!";

很快,您就会想要摆脱DoSomething 并要求所有工作都在MyThingsToDo 注册。然后,您需要向每个ThingToDo 传递一个enum 或其他东西,告诉它为什么这样做。到那时,你会发现你有一个message pump。

顺便说一句,我想说node.js 掩盖了它在多线程的操作系统和应用程序上运行的事实。否则,对网络或磁盘的每次调用都会阻塞。

【讨论】:

您的示例是非阻塞和异步的吗?我所看到的只是一个遍历任务列表的程序。而任务还是以闭包的形式。 @Jeffrey - 是的。您可以通过将阻塞调用委托给 EventLoop 运行来推迟阻塞调用,这意味着您可以在阻塞调用之前运行自己的任务。然而,仔细观察 node.js - 他们实际上确实利用了多个线程(“Node 告诉操作系统(通过 epoll、kqueue、/dev/poll 或 select)它应该被通知当 2 秒结束时”),他们只是没有明确地创建它们。 我想我现在完全糊涂了。 事件循环是非阻塞的,在一个线程中运行。看这里youtube.com/watch?v=QgwSUtYSUqA,so上面的实现是正确的node.js并不是实现事件循环的唯一方式【参考方案4】:

Reactive Extensions for .NET (Rx) 专为异步和并行编程而设计。它允许您以无阻塞的反应式和交互式方式进行编程。您使用 LINQ 查询运算符,以及用于 IObservable/IObserver 接口的新运算符,它们是 Rx 的一部分。 Rx 以 IObservable/IObserver 的形式提供 IEnumerable/IEnumerator 的数学对偶,这意味着您可以以声明的方式使用所有 LINQ 标准查询运算符,而不是直接使用多线程 API。

【讨论】:

然而,Rx 是完全多线程的,尽管原则上它也可以支持单线程编程模型。我对相同想法的实现是单线程的(但在很多方面受到限制!)tomasp.net/blog/reactive-iv-reactivegame.aspx。 Rx 现在也可用于 javascript,这当然是单线程的 :-)。 由于对异步需要或暗示多线程有很多困惑,让我明确一点,异步并不暗示或需要多线程,Rx 也没有。例如,请参阅blogs.msdn.com/b/ericlippert/archive/2010/11/04/…,了解有关其工作原理的信息。【参考方案5】:

不确定this 是否是您要查找的内容。 .NET 可以在没有显式多线程的情况下执行异步回调。

【讨论】:

我不是 100% 确定,但我有点认为 .NET 会在后台创建新线程来异步运行事件,但我可能错了。【参考方案6】:

您的 node.js 示例并不真正适用,因为它运行的服务器正在执行所有必要的多线程。如果事件是基于相同的外部时钟信号执行的,那么它们就不是异步的。您可以通过运行另一个应用程序来解决这个问题,这会在系统上创建两个进程。

没有办法让同一个应用程序作为单个系统上的单个进程运行而无需另一个线程来执行异步事件。

更多详情请看Asynchronous vs Multithreading question。

【讨论】:

看来我需要亲自动手,开始解读事件机、twisted 和 node.js 等库的底层。我有兴趣真正了解它们的工作原理。【参考方案7】:

即使您不做 UI,也可以使用 WPF 调度程序。引用程序集WindowsBase。然后,在你的启动代码中执行:

Dispatcher.CurrentDispatcher.BeginInvoke(new Action(Initialize));
Dispatcher.Run();

Initialize 和代码中的其他地方,使用Dispatcher.CurrentDispatcher.BeginInvoke 来安排后续异步方法的执行。

【讨论】:

【参考方案8】:

我开发了一个基于 HttpListener 和一个事件循环的服务器,支持 MVC、WebApi 和路由。就我所见,性能远远好于标准 IIS+MVC,对于 MVCMusicStore,我从每秒 100 个请求和 100% CPU 移动到 350 个 30% CPU。 如果有人愿意尝试,我正在努力寻求反馈!

PS 欢迎大家提出建议或更正!

Documentation MvcMusicStore sample port on Node.Cs Packages on Nuget

【讨论】:

【参考方案9】:

我相信这是可能的,这是一个用 VB.NET 和 C# 编写的开源示例:

https://github.com/perrybutler/dotnetsockets/

它使用Event-based Asynchronous Pattern (EAP)、IAsyncResult Pattern 和线程池(IOCP)。它将消息(消息可以是任何本机对象,例如类实例)序列化/编组为二进制数据包,通过 TCP 传输数据包,然后在接收端反序列化/解组数据包,以便您使用本机对象.这部分有点像 Protobuf 或 RPC。

它最初是作为实时多人游戏的“网络代码”开发的,但它可以用于多种用途。不幸的是,我从来没有开始使用它。也许其他人会。

源代码有很多 cmets,所以应该很容易理解。享受吧!

编辑:经过压力测试和一些优化后,它能够在达到操作系统限制之前接受 16,357 个客户端。结果如下:

Simulating 1000 client connections over 16 iterations...
1) 485.0278 ms
2) 452.0259 ms
3) 495.0283 ms
4) 476.0272 ms
5) 472.027 ms
6) 477.0273 ms
7) 522.0299 ms
8) 516.0295 ms
9) 457.0261 ms
10) 506.029 ms
11) 474.0271 ms
12) 496.0283 ms
13) 545.0312 ms
14) 516.0295 ms
15) 517.0296 ms
16) 540.0309 ms
All iterations complete. Total duration: 7949.4547 ms

现在所有客户端都在本地主机上运行,​​并在连接后向服务器发送一条小消息。 在另一个系统上进行的后续测试中,服务器以每秒大约 2000 个的速度达到超过 64,000 个客户端连接(达到端口限制!),消耗了 238 MB 的 RAM。

【讨论】:

从技术上讲这仍然是多线程,只是库正在为你做。 感谢您的澄清,您在技术上是正确的。但是,它与 Node.js 在 Windows 中用于处理完成(线程池/IOCP)的方法相同,至少我认为是这样,这意味着 Node.js 也不是真正的单线程。因此,我确实认为 C# 能够与 Node.js 进行类似的设计,以试图回答最初的问题。虽然线程池/IOCP 确实意味着使用多个线程,但它仍然不被视为“线程服务器”。你怎么看? 你是对的,在后台节点does use a C++-based thread pool for long-running tasks。我不知道。 (突然之间,它在建筑上似乎不那么有趣了……) 这是一篇很棒的文章,大卫,我很高兴不久前阅读了它。 Node.js 使用 libuv,这是一个抽象跨各种操作系统的异步 IO 操作的库,它为 Windows、Unix、OSX 等中 Node.js 背后的异步功能提供了动力。如果你问我,这就是神奇之处在 Node.js 中,它是 Javascript 的事实,这意味着客户端代码可以很容易地移植到服务器。 也就是说,我认为对于很多人来说,Node.js 仍然是一头神秘的野兽。【参考方案10】:

这里是单线程 EAP 的my example。在不同的应用程序类型中有几种 EAP 实现:从简单的控制台应用程序到 asp.net 应用程序、tcp 服务器等等。它们都建立在名为 SingleSand 的微型框架上,理论上可以插入任何类型的 .net 应用程序。

与之前的答案相比,我的实现充当了一侧的现有技术(asp.net、tcp 套接字、RabbitMQ)与另一侧的事件循环内的任务之间的中介。所以目标不是做一个纯粹的单线程应用程序,而是将事件循环集成到现有的应用程序技术中。我完全同意之前的帖子,即 .net 不支持纯单线程应用程序。

【讨论】:

以上是关于.NET 中是不是可以使用 C# 实现基于事件的异步模式而无需多线程?的主要内容,如果未能解决你的问题,请参考以下文章

asp.net中普通超链接如何触发C#事件

C# 事件

C# asp.net WebForm 的三层架构配合ListView实现增删改查源码

.NET 准则的事件(C# 编程指南)

.Net C# 接口,了解只需3分钟

C#详解事件