19

내가 동기 apis와 스레드 풀을 사용하여 이런 식으로 보이는 TCP 서버에서 일하고 :

TcpListener listener;
void Serve(){
  while(true){
    var client = listener.AcceptTcpClient();
    ThreadPool.QueueUserWorkItem(this.HandleConnection, client);
    //Or alternatively new Thread(HandleConnection).Start(client)
  }
}

내 목표는 리소스 사용량이 가장 적은 동시 연결을 가능한 한 많이 처리하는 것이라고 가정 할 때 사용 가능한 스레드 수에 따라 재빨리 제한되는 것 같습니다. Non-blocking Task API를 사용하면 더 적은 자원으로 더 많은 것을 처리 할 수있을 것으로 생각됩니다.

저의 첫인상은 다음과 같습니다.

async Task Serve(){
  while(true){
    var client = await listener.AcceptTcpClientAsync();
    HandleConnectionAsync(client); //fire and forget?
  }
}

그러나 이것은 병목 현상을 일으킬 수 있다고 생각합니다. 아마도 HandleConnectionAsync는 첫 번째 기다리기에 비정상적으로 긴 시간이 걸릴 것이고 주 수락 루프가 진행되지 않게 할 것입니다. 이것은 오직 하나의 쓰레드만을 사용합니까, 아니면 런타임에 마술처럼 여러 쓰레드에서 일을 실행합니까?

이 두 가지 접근 방식을 결합하여 서버가 실제로 실행중인 작업의 수만큼 필요한 스레드 수를 사용하지만 IO 작업에서 불필요하게 스레드를 차단하지 않도록 할 수 있습니까?

이와 같은 상황에서 처리량을 극대화하는 관용적 인 방법이 있습니까?


  • async / await는 현재 스레드 afaik에서 작동합니다. 작업 풀에서 일정을 예약하려면 작업을 명시 적으로 사용해야합니다. 말하자면 현재의 구현은 순전히 단일 스레드입니다. - cwharris
  • 이것은 모든 답변을 줄 수 있습니다 :소켓 작업 대기 중. - noseratio
  • @Noseratio, 전혀 다른 질문에 답하는 것이지만 어떻게 해결되는지는 모르겠다. 이 기사에서는 Task 기반 API를 노출 한 것처럼 서로 다른 비동기 패턴을 노출하는 api를 기다리는 것에 대해 다룹니다. TcpListener는 실제로 Task 기반 api를 제공합니다. - captncraig
  • 의심 할 여지없이 처음에는 질문을 이해하지 못했습니다. 더 나은 이해와 함께, 여기 있습니다.내 생각들. - noseratio

5 답변


22

프로파일 링 테스트에서 필자가 필요로하지 않는다면 프레임 워크가 스레딩을 관리하게하고 추가 스레드를 생성하지 않을 것입니다. 특히, 내부 통화HandleConnectionAsync대부분 IO 기반입니다.

어쨌든, 시작 부분에 호출 스레드 (디스패처)를 해제하려면HandleConnectionAsync, 매우 쉬운 해결책이 있습니다.에서 새 스레드에 이동할 수 있습니다.ThreadPoolawait Yield().이것은 서버가 일반적으로 TCP 서버의 경우 인 초기 스레드 (콘솔 응용 프로그램, WCF 서비스)에 설치된 동기화 컨텍스트가없는 실행 환경에서 실행되는 경우 작동합니다.

[편집 됨]다음은 이것을 설명합니다 (코드는 원래이리). 참고로, 메인while루프는 명시 적으로 스레드를 생성하지 않습니다.

using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class Program
{
    object _lock = new Object(); // sync lock 
    List<Task> _connections = new List<Task>(); // pending connections

    // The core server task
    private async Task StartListener()
    {
        var tcpListener = TcpListener.Create(8000);
        tcpListener.Start();
        while (true)
        {
            var tcpClient = await tcpListener.AcceptTcpClientAsync();
            Console.WriteLine("[Server] Client has connected");
            var task = StartHandleConnectionAsync(tcpClient);
            // if already faulted, re-throw any error on the calling context
            if (task.IsFaulted)
                task.Wait();
        }
    }

    // Register and handle the connection
    private async Task StartHandleConnectionAsync(TcpClient tcpClient)
    {
        // start the new connection task
        var connectionTask = HandleConnectionAsync(tcpClient);

        // add it to the list of pending task 
        lock (_lock)
            _connections.Add(connectionTask);

        // catch all errors of HandleConnectionAsync
        try
        {
            await connectionTask;
            // we may be on another thread after "await"
        }
        catch (Exception ex)
        {
            // log the error
            Console.WriteLine(ex.ToString());
        }
        finally
        {
            // remove pending task
            lock (_lock)
                _connections.Remove(connectionTask);
        }
    }

    // Handle new connection
    private async Task HandleConnectionAsync(TcpClient tcpClient)
    {
        await Task.Yield();
        // continue asynchronously on another threads

        using (var networkStream = tcpClient.GetStream())
        {
            var buffer = new byte[4096];
            Console.WriteLine("[Server] Reading from client");
            var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length);
            var request = Encoding.UTF8.GetString(buffer, 0, byteCount);
            Console.WriteLine("[Server] Client wrote {0}", request);
            var serverResponseBytes = Encoding.UTF8.GetBytes("Hello from server");
            await networkStream.WriteAsync(serverResponseBytes, 0, serverResponseBytes.Length);
            Console.WriteLine("[Server] Response has been written");
        }
    }

    // The entry point of the console app
    static void Main(string[] args)
    {
        Console.WriteLine("Hit Ctrl-C to exit.");
        new Program().StartListener().Wait();
    }
}

또는 코드는 다음과 같을 수 있습니다.await Task.Yield(). 참고, 나는 패스한다.~async람다Task.Run내가 아직도하고 싶어서.내부의 비동기 API 활용HandleConnectionAsync사용await거기에:

// Handle new connection
private static Task HandleConnectionAsync(TcpClient tcpClient)
{
    return Task.Run(async () =>
    {
        using (var networkStream = tcpClient.GetStream())
        {
            var buffer = new byte[4096];
            Console.WriteLine("[Server] Reading from client");
            var byteCount = await networkStream.ReadAsync(buffer, 0, buffer.Length);
            var request = Encoding.UTF8.GetString(buffer, 0, byteCount);
            Console.WriteLine("[Server] Client wrote {0}", request);
            var serverResponseBytes = Encoding.UTF8.GetBytes("Hello from server");
            await networkStream.WriteAsync(serverResponseBytes, 0, serverResponseBytes.Length);
            Console.WriteLine("[Server] Response has been written");
        }
    });
}

[최신 정보]주석에 기반하여 : 이것이 라이브러리 코드가 될 것이라면, 실행 환경은 실제로 알려지지 않았고 기본이 아닌 동기화 컨텍스트를 가질 수 있습니다. 이 경우에는 (모든 동기화 컨텍스트가없는) 풀 스레드에서 주 서버 루프를 실행하는 것이 좋습니다.

private static Task StartListener()
{
    return Task.Run(async () => 
    {
        var tcpListener = TcpListener.Create(8000);
        tcpListener.Start();
        while (true)
        {
            var tcpClient = await tcpListener.AcceptTcpClientAsync();
            Console.WriteLine("[Server] Client has connected");
            var task = StartHandleConnectionAsync(tcpClient);
            // if already faulted, re-throw any error on the calling context
            if (task.IsFaulted)
                task.Wait();
        }
    });
}

이렇게하면 내부에서 생성 된 모든 하위 작업StartListener클라이언트 코드의 동기화 컨텍스트에 영향을받지 않습니다. 그래서 나는 전화 할 필요가 없다.Task.ConfigureAwait(false)어디서나 명시 적으로.  


  • 매우 흥미로운. 실행되는 환경 (라이브러리에 있음)을 보장 할 수 없다면 Task.Yield에 대한 문서에서 SynchronizatonContext가 올바르게 다시 전환되지 않는다고 말하는 것과 같습니다. Task.Run이 내 가장 안전한 옵션 일 수 있습니다. - captncraig
  • @captncraig, 오피스는UI 스레드(await Task.Yeild()). 그 이유는 syn가 있기 때문입니다. UI 스레드의 컨텍스트는 다음을 사용합니다.PostMessage이 깊은 내부는 마우스 및 키보드와 같은 다른 사용자 입력 메시지를 인계하고 UI를 차단할 수 있습니다. 이 모든 것은 상황이없는 환경에는 적용되지 않습니다.Task.Yield()단지 사용ThreadPool.QueueUserWorkItem편리한 지름길입니다. 몇 가지 추가 정보 :stackoverflow.com/q/20319769/1768303 - noseratio
  • @Noseratio 당신은 사용 연장을 기대할 수 있습니까?Task.Run코드 안에 있습니까? 내 이해로서 이것은 주로 I / O 작업으로TCPClient데이터를 수신합니다. 왜 새로운 것이 있어야 하는가?ThreadPool쓰레드 파이어 링while (true)고리? - Yuval Itzchakov
  • @ YuvalItzchakov,                         클라이언트 코드가 호출 스레드에서 가질 수있는 모든 동기화 컨텍스트 그것은 한 번만 할 수있는 것으로,await tcpListener.AcceptTcpClientAsync().ConfigureAwait(false). - noseratio
  • 큰. 설명 주셔서 감사합니다! - Yuval Itzchakov

8

기존 답변을 올바르게 사용하도록 제안했습니다.Task.Run(() => HandleConnection(client));그러나 이유는 설명하지 않았습니다.

이유는 다음과 같습니다.HandleConnectionAsync첫 번째를 기다리는 데 약간의 시간이 걸릴 수 있습니다. 비동기 IO를 사용하는 경우 (이 경우처럼) 이는HandleConnectionAsync차단하지 않고 CPU 바운드 작업을 수행하고 있습니다. 이것은 스레드 풀에 대한 완벽한 경우입니다. 짧은 논 블로킹 CPU 작업을 실행하도록 만들어졌습니다.

그리고 여러분은 맞습니다. accept 루프는HandleConnectionAsync돌아 오기 전에 오랜 시간이 걸릴 것입니다 (어쩌면 CPU에 상당한 작업이 있기 때문일 수도 있습니다). 새로운 연결 빈도가 높아야하는 경우에는이를 피하십시오.

루프를 조절하는 중요한 작업이 없다고 확신하는 경우 추가 스레드 풀을 저장할 수 있습니다Task하지 마라.

또는 동시에 여러 수락을 실행할 수 있습니다. 바꾸다await Serve();(예를 들어) :

var serverTasks =
    Enumerable.Range(0, Environment.ProcessorCount)
    .Select(_ => Serve());
await Task.WhenAll(serverTasks);

이렇게하면 확장 성 문제가 해결됩니다.주목해라.await여기에 하나의 오류를 제외한 모든 것을 삼킨다.


1

시험

TcpListener listener;
void Serve(){
  while(true){
    var client = listener.AcceptTcpClient();
    Task.Run(() => this.HandleConnection(client));
    //Or alternatively new Thread(HandleConnection).Start(client)
  }
}


  • 이 질문에 어떻게 대답합니까? - Leri
  • 원래의 구현과 마찬가지로 모든 연결에 대해 새로운 스레드를 생성 할 수 있습니까? - captncraig
  • Tasks를 사용하여이 작업을 수행하려고한다고 생각했습니다. "스레드 풀 서버"로 수행 할 수있는 작업이 없습니다. 스레드 수를 제한 할 수 있습니다. 각 IO 호출에 대해 논 블로킹 API를 사용해야합니다. - Aron
  • " 모든 연결에 대해 새 스레드를 본질적으로 생성하지 않습니다 " 아니. 새로운 것을 산산조각 내고있다.태스크, 하나의 작업이 하나의 작업과 동일하지 않습니다.. 하나의 스레드는 둘 이상의 태스크를 처리 할 수 있으며 필요에 따라 새로운 스레드가 생성됩니다. - DasKrümelmonster
  • Task.Run은 분명합니다.아니새 스레드를 시작하는 것과 같습니다. Task.Run은 ThreadPool의 기존 스레드를 사용하지만 Thread.Start는 재사용하지 않을 새 스레드에 별표를 표시합니다. - Panagiotis Kanavos

1

Microsoft에 따르면http://msdn.microsoft.com/en-AU/library/hh524395.aspx#BKMK_VoidReturnType예외를 캐치 할 수 없기 때문에 void 반환 형식을 사용하면 안됩니다. 필자가 지적했듯이 "불과 잊기"작업이 필요하므로 내 결론은 작업 (Microsoft가 말했듯이)을 항상 반환해야한다는 것입니다. 그러나 다음을 사용하여 오류를 잡아야합니다.

TaskInstance.ContinueWith(i => { /* exception handler */ }, TaskContinuationOptions.OnlyOnFaulted);

증거로 사용한 예가 아래에 있습니다.

public static void Main()
{
    Awaitable()
        .ContinueWith(
            i =>
                {
                    foreach (var exception in i.Exception.InnerExceptions)
                    {
                        Console.WriteLine(exception.Message);
                    }
                },
            TaskContinuationOptions.OnlyOnFaulted);
    Console.WriteLine("This needs to come out before my exception");
    Console.ReadLine();
}

public static async Task Awaitable()
{
    await Task.Delay(3000);
    throw new Exception("Hey I can catch these pesky things");
}


0

비동기 연결을 수락해야하는 이유가 있습니까? 내 말은, 어떤 클라이언트 연결을 기다리는 것이 당신에게 어떤 가치를 부여 할까? 이 작업을 수행하는 유일한 이유는 연결을 기다리는 동안 서버에서 진행중인 다른 작업이 있기 때문입니다. 만약 당신이 아마 이런 일을 할 수있다 :

    public async void Serve()
    {
        while (true)
        {
            var client = await _listener.AcceptTcpClientAsync();
            Task.Factory.StartNew(() => HandleClient(client), TaskCreationOptions.LongRunning);
        }
    }

이 방법은 accepting은 다른 것들을위한 현재의 thread leave 옵션을 해제 할 것이고, 핸들링은 새로운 쓰레드에서 수행 될 것이다. 유일한 오버 헤드는 새로운 연결을 받아들이 기 직전에 클라이언트를 처리하기위한 새로운 스레드를 생성하는 것입니다.

편집하다: 당신이 쓴 코드와 거의 같은 코드라는 것을 깨달았습니다. 내가 실제로 묻는 내용을 더 잘 이해하려면 질문을 다시 읽어야한다고 생각합니다. S

편집 2 :

이 두 가지 접근 방식을 결합하여 내 서버에서      활발히 실행되는 작업의 수에 대해 필요한 스레드의 수는      IO 작업에 불필요하게 스레드를 차단하지 않습니까?

내 솔루션이 실제로이 질문에 대답한다고 생각하십시오. 그래도 정말 필요한가요?

편집 3 : Task.Factory.StartNew ()는 실제로 새 스레드를 만듭니다.


  • 연결을 비동기 적으로 받아들이는 것은 아무것도하지 않고 연결을 기다리는 쓰레드를 낭비하지 않음으로써 가치를 추가합니다. 대부분의 경우 연결 대기는 실제로 요청을 처리하는 것보다 훨씬 오래 걸립니다. 또한 Task.StartNew는아니새 스레드를 생성하지만 ThreadPool의 스레드를 사용합니다. - Panagiotis Kanavos
  • 물론 다른 일이 있다면 그것은 가치를 더합니다. 그러므로 원래의 대답에 대한 제 질문입니다. 차단 된 스레드와 중단 된 스레드 사이에 큰 차이가 없으면 수행 할 작업이 없다면 실제로 이득을 볼 수 없습니다. TaskCreationOptions.LongRunning에 대한 힌트는 새로운 스레드에서 시작해야하지만, 당신은 그것에 대해 옳았다. - EMB

연결된 질문


관련된 질문

최근 질문