Development Tip

.NET에서 비 차단, 단일 스레드, 비동기 웹 서버 (예 : Node.js)가 가능합니까?

yourdevel 2020. 12. 25. 10:36
반응형

.NET에서 비 차단, 단일 스레드, 비동기 웹 서버 (예 : Node.js)가 가능합니까?


.NET에서 단일 스레드 , 이벤트 기반 비 차단 비동기 웹 서버 를 만드는 방법을 찾고있는 이 질문 을보고있었습니다 .

이 답변 은 코드 본문이 단일 스레드에서 실행된다고 주장함으로써 처음에는 유망 해 보였습니다.

그러나 나는 이것을 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에서 단일 스레드 , 이벤트 기반 비 차단 비동기 웹 서버를 어떻게 얻을 수 있습니까?


전체 SetSynchronizationContext는 붉은 청어입니다. 이것은 마샬링을위한 메커니즘 일뿐입니다. 작업은 여전히 ​​IO 스레드 풀에서 발생합니다.

당신이 요구하는 것은 메인 스레드에서 모든 IO 작업에 대한 비동기 프로 시저 호출 을 대기열에 넣고 수집하는 방법 입니다. 많은 상위 레벨 프레임 워크가 이러한 종류의 기능을 래핑하며 가장 유명한 것은 libevent 입니다.

여기에 다양한 옵션에 대한 큰 요약이 있습니다. epoll, poll, threadpool의 차이점은 무엇입니까? .

.NET은 BeginXYZ메서드 를 호출 할 때 IO 액세스를 처리하는 특수 "IO 스레드 풀"을 통해 이미 확장을 처리 합니다. 이 IO 스레드 풀에는 상자의 프로세서 당 최소 1 개의 스레드가 있어야합니다. 참조 : ThreadPool.SetMaxThreads .

단일 스레드 앱이 중요한 요구 사항 인 경우 (미친 이유로) 물론 DllImport 를 사용하여이 모든 항목을 상호 운용 할 수 있습니다 ( 여기 에서 예제 참조 ).

그러나 이것은 매우 복잡하고 위험한 작업입니다 .

완료 메커니즘으로 APC를 지원하지 않는 이유는 무엇입니까? APC는 실제로 사용자 코드를위한 좋은 범용 완성 메커니즘이 아닙니다. APC에 의해 도입 된 재진입을 관리하는 것은 거의 불가능합니다. 예를 들어, 잠금을 차단할 때마다 임의의 I / O 완료가 스레드를 차지할 수 있습니다. 자체 잠금을 획득하려고 시도 할 수 있으며, 이로 인해 잠금 순서 문제가 발생하여 교착 상태가 발생할 수 있습니다. 이를 방지하려면 세심한 설계가 필요하며 경고 대기 중에 다른 사람의 코드가 실행되지 않도록하는 능력이 필요하며 그 반대의 경우도 마찬가지입니다. 이것은 APC의 유용성을 크게 제한합니다.

그래서 요약하자. APC 및 완료 포트를 사용하여 모든 작업을 수행 하는 단일 스레드 관리 프로세스 를 원한다면 직접 코딩해야합니다. 그것을 만드는 것은 위험하고 까다로울 것입니다.

단순히 높은 규모의 네트워킹을 원한다면 BeginXYZAPC를 사용하기 때문에 계속 해서 가족과 함께 사용할 수 있으며 잘 수행 될 것입니다. 스레드와 .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에서 차단 될 때 (예 : 처리의 일부로 파일에서 읽을 때) 응용 프로그램이 다른 스레드를 활성화 할 수있는 방법이 있어야합니다.


당신이 필요로하는 것은 큐에있는 다음 태스크를 취하고 그것을 실행하는 "메시지 루프"입니다. 또한 모든 작업은 차단없이 최대한 많은 작업을 완료하도록 코딩 한 다음 나중에 시간이 필요한 작업을 선택하기 위해 추가 작업을 대기열에 추가해야합니다. 이것에 대해 마법 같은 것은 없습니다. 차단 호출을 사용하지 않고 추가 스레드를 생성하지 않습니다.

예를 들어, HTTP GET을 처리 할 때 서버는 소켓에서 현재 사용 가능한만큼의 데이터를 읽을 수 있습니다. 이 데이터가 요청을 처리하기에 충분하지 않으면 나중에 소켓에서 다시 읽을 수 있도록 새 작업을 대기열에 추가합니다. FileStream의 경우 인스턴스의 ReadTimeout을 낮은 값으로 설정하고 전체 파일보다 적은 바이트를 읽을 수 있도록 준비하려고합니다.

C # 5는 실제로이 패턴을 훨씬 더 사소하게 만듭니다. 많은 사람들이 비동기 기능이 멀티 스레딩을 의미 한다고 생각 하지만 그렇지 않습니다 . 비동기를 사용하면 기본적으로 앞서 언급 한 작업 대기열을 관리 할 필요없이 얻을 수 있습니다.


네, 이름은 Manos de mono

진지하게, manos의 전체 아이디어는 단일 스레드 비동기 이벤트 구동 웹 서버입니다.

고성능 및 확장 가능. 친구 피드를 지원하는 기술인 tornadoweb을 모델로 한 Manos는 수천 개의 동시 연결이 가능하며 서버와의 지속적인 연결을 생성하는 애플리케이션에 이상적입니다.

이 프로젝트는 유지 관리가 적고 생산 준비가되지 않았을 것 같지만 이것이 가능하다는 것을 보여주는 좋은 사례 연구가됩니다.


다음은 IO 완료 포트가 무엇이며 C #을 통해 액세스 할 수있는 방법을 설명하는 훌륭한 기사 시리즈입니다 (예 : Kernel32.dll에서 Win32 API 호출로 PInvoke해야 함).

참고 : node.js 뒤의 교차 플랫폼 IO 프레임 워크 인 libuv 는 Windows에서 IOCP를 사용하고 유닉스 운영 체제에서 libev를 사용합니다.

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


아무도 카약언급하지 않았 는지 궁금 합니다. 기본적으로 Pythons twisted , JavaScripts node.js 또는 Rubys eventmachine에 대한 C #의 답변입니다.


나는 그러한 아키텍처의 내 자신의 간단한 구현을 다루어 왔으며 github 에 올려 놓았습니다 . 나는 그것을 배우는 것으로 더하고있다. 그러나 그것은 많은 즐거움이었고 나는 그것을 더 플러시 할 것이라고 생각합니다.

매우 알파이므로 변경 될 수 있지만 코드는 다음과 같습니다.

   //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");

   });

이에 대한 자세한 정보는 여기 에서 찾을 수 있습니다 .


MVC, WebApi 및 라우팅을 지원하는 HttpListener 및 이벤트 루프를 기반으로 서버를 개발했습니다. 내가 본 성능은 표준 IIS + MVC보다 훨씬 낫습니다. 누군가가 그것을 시도한다면 나는 피드백을 위해 고군분투하고 있습니다! 실제로이 구조를 기반으로 웹 사이트를 만드는 템플릿이 있습니다.

절대적으로 필요할 때까지 ASYNC / AWAIT를 사용하지 않습니다. 내가 사용하는 유일한 작업은 소켓에 쓰기 또는 파일 읽기와 같은 I / O 바인딩 작업을위한 작업입니다.

PS 어떤 제안이나 수정도 환영합니다!


이 프레임 워크 SignalR 과이 블로그 에 대해


여기에서는 운영 체제의 지원이 필수적입니다. 예를 들어 Mono는 비동기 I / O가있는 Linux에서 epoll을 사용하므로 확장 성이 매우 우수해야합니다 (여전히 스레드 풀). 당신이 찾고 있고 성능과 확장 성을 원한다면 꼭 시도하십시오.

반면에 언급 한 아이디어를 기반으로하는 C # (네이티브 라이브러리 포함) 웹 서버의 예는 Manos de Mono 일 수 있습니다. 프로젝트가 최근에 활성화되지 않았습니다. 그러나 아이디어와 코드는 일반적으로 사용할 수 있습니다. 이것을 읽으십시오 (특히 "마 노스 자세히보기"부분).

편집하다:

주 스레드에서 콜백을 시작하려면 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);
        }
    }
}

원하는 것을 인쇄합니다. 또한 디스패처로 우선 순위를 설정할 수 있습니다. 그러나 동의하십시오, 이것은 추악하고 해키이며 데모 요청에 응답하는 것보다 다른 이유로 왜 그렇게 할 것인지 모르겠습니다.)


먼저 SynchronizationContext에 대해 설명합니다. Sam이 쓴 것과 똑같습니다. 기본 클래스는 단일 스레드 기능을 제공하지 않습니다. UI 스레드에서 코드를 실행하는 기능을 제공하는 WindowsFormsSynchronizationContext에서 이러한 아이디어를 얻었을 것입니다.

여기에서 더 많은 것을 읽을 수 있습니다

ThreadPool 매개 변수와 함께 작동하는 코드를 작성했습니다. (Sam이 이미 지적한 것).

이 코드는 자유 스레드에서 실행할 3 개의 비동기 작업을 등록합니다. 둘 중 하나가 ThreadPool 매개 변수를 변경할 때까지 병렬로 실행됩니다. 그런 다음 각 작업이 동일한 스레드에서 실행됩니다.

.net 앱이 하나의 스레드를 사용하도록 강제 할 수 있다는 것을 증명할뿐입니다. 하나의 스레드에서만 호출을 수신하고 처리하는 웹 서버의 실제 구현은 완전히 다릅니다. :).

코드는 다음과 같습니다.

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();
            }
        }
    }
}

LibuvSharp is a wrapper for libuv, which is used in the node.js project for async IO. BUt it only contains only low level TCP/UDP/Pipe/Timer functionality. And it will stay like that, writing a webserver on top of it is an entire different story. It doesn't even support dns resolving, since this is just a protocol on top of udp.


I believe it's possible, here is an open-source example written in VB.NET and C#:

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

It uses Event-based Asynchronous Pattern (EAP), IAsyncResult Pattern and thread pool (IOCP). It will serialize/marshal the messages (messages can be any native object such as a class instance) into binary packets, transfer the packets over TCP, and then deserialize/unmarshal the packets at the receiving end so you get your native object to work with. This part is somewhat like Protobuf or RPC.

It was originally developed as a "netcode" for real-time multiplayer gaming, but it can serve many purposes. Unfortunately I never got around to using it. Maybe someone else will.

The source code has a lot of comments so it should be easy to follow. Enjoy!


Here is one more implementation of the event-loop web server called SingleSand. It executes all custom logic inside single-threaded event loop but the web server is hosted in asp.net. Answering the question, it is generally not possible to run a pure single threaded app because of .NET multi-threaded nature. There are some activities that run in separate threads and developer cannot change their behavior.

ReferenceURL : https://stackoverflow.com/questions/8905860/is-a-non-blocking-single-threaded-asynchronous-web-server-like-node-js-possi

반응형