.NET中是否可以使用非阻塞,单线程,异步Web服务器(如Node.js)?

时间:2012-01-18 06:03:15

标签: c# multithreading events node.js asynchronous

我在查看this question,正在寻找一种在.NET中创建基于事件的非阻塞异步Web服务器单线程的方法。

This answer首先看起来很有希望,声称代码的主体在一个线程中运行。

但是,我在C#中测试了这个:

using System;
using System.IO;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

        var sc = new SynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(sc);
        {
            var path = Environment.ExpandEnvironmentVariables(
                @"%SystemRoot%\Notepad.exe");
            var fs = new FileStream(path, FileMode.Open,
                FileAccess.Read, FileShare.ReadWrite, 1024 * 4, true);
            var bytes = new byte[1024];
            fs.BeginRead(bytes, 0, bytes.Length, ar =>
            {
                sc.Post(dummy =>
                {
                    var res = fs.EndRead(ar);

                    // Are we in the same thread?
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                }, null);
            }, null);
        }
        Thread.Sleep(100);
    }
}

结果是:

  

1
  5

所以看起来,与答案相反,启动读取的线程和结束读取的线程相同。

所以现在我的问题是,如何在.NET中实现单线程,基于事件的非阻塞异步Web服务器?

13 个答案:

答案 0 :(得分:40)

整个SetSynchronizationContext是一个红色的鲱鱼,这只是一种编组机制,工作仍然发生在IO线程池中。

您要求的是从主线程中为所有IO工作排队并获取Asynchronous Procedure Calls的方法。许多更高级别的框架包含了这种功能,最着名的是libevent

这里有各种选项的重大回顾:Whats the difference between epoll, poll, threadpool?

.NET已经通过一个特殊的“IO线程池”来处理扩展,当你调用BeginXYZ方法时,它会处理IO访问。此IO线程池必须在每个处理器上至少有1个线程。见:ThreadPool.SetMaxThreads

如果单线程应用程序是关键要求(出于某种疯狂的原因),您当然可以使用DllImport来处理所有这些内容(请参阅example here

然而,这将是一个非常complex and risky task

  

为什么我们不支持APC作为完成机制?对于用户代码,APC实际上不是一个很好的通用完成机制。管理APC引入的重入几乎是不可能的;例如,任何时候阻塞锁定,一些任意的I / O完成都可能会占用你的线程。它可能会尝试获取自己的锁,这可能会引入锁定顺序问题,从而导致死锁。防止这种情况需要精心设计,并且能够确保在您的警报等待期间其他人的代码永远不会运行,反之亦然。这极大地限制了APC的有用性。

所以,回顾一下。如果您希望单线程托管进程使用APC和完成端口完成所有工作,您将不得不手动编写代码。建立它将是冒险和棘手的。

如果您只是想要高规模网络,您可以继续使用BeginXYZ和家人,并放心它会表现良好,因为它使用了APC。你在线程和.NET特定实现之间支付一个小的价格编组。

来自:http://msdn.microsoft.com/en-us/magazine/cc300760.aspx

  

扩展服务器的下一步是使用异步I / O.异步I / O减少了创建和管理线程的需要。这导致更简单的代码,也是一种更高效的I / O模型。异步I / O利用回调来处理传入的数据和连接,这意味着没有要设置和扫描的列表,也没有必要创建新的工作线程来处理挂起的I / O.

一个有趣的事实是,单线程不是使用完成端口在Windows上执行异步套接字的最快方法,请参阅:http://doc.sch130.nsc.ru/www.sysinternals.com/ntw2k/info/comport.shtml

  

服务器的目标是通过让线程避免不必要的阻塞来尽可能少地进行上下文切换,同时通过使用多个线程来最大化并行性。理想情况是,在每个处理器上有一个主动服务于客户端请求的线程,并且如果在完成请求时有其他请求等待,那么这些线程不会阻塞。然而,为了使其正常工作,当处理客户端请求在I / O上阻塞时(例如,当它作为处理的一部分从文件读取时),应用程序必须有一种方法来激活另一个线程。

答案 1 :(得分:17)

你需要的是一个“消息循环”,它接受队列上的下一个任务并执行它。此外,每个任务都需要进行编码,以便在不阻塞的情况下完成尽可能多的工作,然后将其他任务排入队列以获取稍后需要时间的任务。这没有什么神奇之处:永远不要使用阻塞调用,也永远不会产生额外的线程。

例如,在处理HTTP GET时,服务器可以读取插槽上当前可用的数据。如果这不足以处理请求,则将新任务排入队列,以便将来再次从套接字读取。对于FileStream,您希望将实例上的ReadTimeout设置为较低的值,并准备读取比整个文件更少的字节。

C#5实际上使这种模式更加微不足道。许多人认为async functionality意味着多线程,但是that is not the case。使用async,您基本上可以获得我之前提到的任务队列,而无需明确管理它。

答案 2 :(得分:10)

是的,它被称为Manos de mono

说真的,manos背后的整个想法是一个单线程异步事件驱动的Web服务器。

  

高性能和可扩展性。仿照tornadoweb(支持朋友馈送的技术),Manos能够实现数千个同时连接,非常适合与服务器建立持久连接的应用程序。

该项目的维护费用似乎很低,可能还没有准备就绪,但它是一个很好的案例研究,可以证明这是可行的。

答案 3 :(得分:7)

这是一篇很棒的文章系列,解释了IO完成端口是什么以及如何通过C#访问它们(即你需要从Kernel32.dll进入PInvoke到Win32 API调用)。

注意:node.js后面的libuv跨平台IO框架在Windows上使用IOCP,在unix操作系统上使用libev。

http://www.theukwebdesigncompany.com/articles/iocp-thread-pooling.php

答案 4 :(得分:2)

我想知道没有人提到kayak它基本上是C#对Pythons twisted,JavaScripts node.js或Rubys eventmachine

的回答

答案 5 :(得分:1)

我一直在摆弄我自己对这种架构的简单实现,我把它放在了github上。我正在做更多的学习。但它很有趣,我想我会更多地将它冲洗掉。

这是非常alpha的,所以它很容易改变,但代码看起来有点像这样:

   //Start the event loop.
   EventLoop.Start(() => {

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

   });

可以找到有关它的更多信息here

答案 6 :(得分:1)

我开发了一个基于HttpListener的服务器和一个事件循环,支持MVC,WebApi和路由。对于我所看到的,性能远远超过标准的IIS + MVC,对于MVCMusicStore,我将每秒100个请求和100%CPU从30%CPU转移到350。 如果有人试一试,我正在努力寻求反馈! 实际上是一个基于这种结构创建网站的模板。

请注意,在绝对必要之前,我不会使用ASYNC / AWAIT。我在那里使用的唯一任务是用于I / O绑定操作的任务,例如在套接字上写或读取文件。

PS欢迎任何建议或更正!

答案 7 :(得分:0)

你可以这个框架SignalR 这个Blog关于它

答案 8 :(得分:0)

操作系统的某种支持在这里至关重要。例如,Mono在Linux上使用epoll和异步I / O,所以它应该可以很好地扩展(仍然是线程池)。如果您正在寻找性能和可扩展性,请务必尝试。

另一方面,基于您提到的想法的C#(带有本机库)webserver的示例可以是Manos de Mono。项目最近没有活跃;但是,通常可以使用想法和代码。阅读this(特别是“仔细看看Manos”部分)。

修改

如果您只想在主线程上触发回调,则可以对WPF调度程序等现有同步上下文进行一些滥用。您的代码,转换为此方法:

using System;
using System.IO;
using System.Threading;
using System.Windows;

namespace Node
{
    class Program
    {
        public static void Main()
        {
            var app = new Application();
            app.Startup += ServerStart;
            app.Run();
        }

        private static void ServerStart(object sender, StartupEventArgs e)
        {
            var dispatcher = ((Application) sender).Dispatcher;
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            var path = Environment.ExpandEnvironmentVariables(
                @"%SystemRoot%\Notepad.exe");
            var fs = new FileStream(path, FileMode.Open,
                FileAccess.Read, FileShare.ReadWrite, 1024 * 4, true);
            var bytes = new byte[1024];
            fs.BeginRead(bytes, 0, bytes.Length, ar =>
            {
                dispatcher.BeginInvoke(new Action(() =>
                {
                    var res = fs.EndRead(ar);

                    // Are we in the same thread?
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                }));
            }, null);
        }
    }
}

打印您想要的内容。另外,您可以使用调度程序设置优先级。但同意,这是丑陋的,hacky,我不知道为什么我会这样做而出于另一个原因而不是回答你的演示请求;)

答案 9 :(得分:0)

首先讲述SynchronizationContext。这就像Sam写的那样。基类不会为您提供单线程功能。您可能从WindowsFormsSynchronizationContext获得了这个想法,它提供了在UI线程上执行代码的功能。

You can read more here

我写了一段与ThreadPool参数一起使用的代码。 (Sam已经指出了这一点)。

此代码注册要在自由线程上执行的3个异步操作。它们并行运行,直到其中一个更改ThreadPool参数。然后每个动作在同一个线程上执行。

它只能证明你可以强制.net app使用一个线程。 仅在一个线程上接收和处理调用的Web服务器的实际实现完全不同:)。

以下是代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;

namespace SingleThreadTest
{
    class Program
    {
        class TestState
        {
            internal string ID { get; set; }
            internal int Count { get; set; }
            internal int ChangeCount { get; set; }
        }

        static ManualResetEvent s_event = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            int nWorkerThreads;
            int nCompletionPortThreads;
            ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionPortThreads);
            Console.WriteLine(String.Format("Max Workers: {0} Ports: {1}",nWorkerThreads,nCompletionPortThreads));
            ThreadPool.GetMinThreads(out nWorkerThreads, out nCompletionPortThreads);
            Console.WriteLine(String.Format("Min Workers: {0} Ports: {1}",nWorkerThreads,nCompletionPortThreads));
            ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), new TestState() { ID = "A  ", Count = 10, ChangeCount = 0 });
            ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), new TestState() { ID = " B ", Count = 10, ChangeCount = 5 });
            ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), new TestState() { ID = "  C", Count = 10, ChangeCount = 0 });
            s_event.WaitOne();
            Console.WriteLine("Press enter...");
            Console.In.ReadLine();
        }

        static void LetsRunLikeCrazy(object o)
        {
            if (s_event.WaitOne(0))
            {
                return;
            }
            TestState oState = o as TestState;
            if (oState != null)
            {
                // Are we in the same thread?
                Console.WriteLine(String.Format("Hello. Start id: {0} in thread: {1}",oState.ID, Thread.CurrentThread.ManagedThreadId));
                Thread.Sleep(1000);
                oState.Count -= 1;
                if (oState.ChangeCount == oState.Count)
                {
                    int nWorkerThreads = 1;
                    int nCompletionPortThreads = 1;
                    ThreadPool.SetMinThreads(nWorkerThreads, nCompletionPortThreads);
                    ThreadPool.SetMaxThreads(nWorkerThreads, nCompletionPortThreads);

                    ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionPortThreads);
                    Console.WriteLine(String.Format("New Max Workers: {0} Ports: {1}", nWorkerThreads, nCompletionPortThreads));
                    ThreadPool.GetMinThreads(out nWorkerThreads, out nCompletionPortThreads);
                    Console.WriteLine(String.Format("New Min Workers: {0} Ports: {1}", nWorkerThreads, nCompletionPortThreads));
                }
                if (oState.Count > 0)
                {
                    Console.WriteLine(String.Format("Hello. End   id: {0} in thread: {1}", oState.ID, Thread.CurrentThread.ManagedThreadId));
                    ThreadPool.QueueUserWorkItem(new WaitCallback(LetsRunLikeCrazy), oState);
                }
                else
                {
                    Console.WriteLine(String.Format("Hello. End   id: {0} in thread: {1}", oState.ID, Thread.CurrentThread.ManagedThreadId));
                    s_event.Set();
                }
            }
            else
            {
                Console.WriteLine("Error !!!");
                s_event.Set();
            }
        }
    }
}

答案 10 :(得分:0)

LibuvSharp是libuv的包装器,在node.js项目中用于异步IO。它只包含低级TCP / UDP /管道/定时器功能。它会保持这样,在它上面写一个网络服务器是一个完全不同的故事。它甚至不支持dns解析,因为这只是udp之上的协议。

答案 11 :(得分:0)

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

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

它使用基于事件的异步模式(EAP),IAsyncResult模式和线程池(IOCP)。它会将消息(消息可以是任何本机对象,如类实例)序列化/编组为二进制数据包,通过TCP传输数据包,然后在接收端反序列化/解组数据包,这样您就可以使用本机对象来处理。这部分有点像Protobuf或RPC。

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

源代码有很多注释,因此应该很容易理解。享受!

答案 12 :(得分:0)

这是另一个名为implementation的事件循环Web服务器的SingleSand。它在单线程事件循环中执行所有自定义逻辑,但Web服务器托管在asp.net中。 回答这个问题,由于.NET多线程特性,通常不可能运行纯单线程应用程序。有some个活动在不同的线程中运行,开发人员无法更改其行为。