web-dev-qa-db-ja.com

ThreadPoolサーバーの非同期/待機に相当するものは何ですか?

私は同期APIとスレッドプールを使用して次のようなtcpサーバーで作業しています。

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

私の目標がリソース使用量を最小限に抑えてできるだけ多くの同時接続を処理することであると仮定すると、これは使用可能なスレッドの数によってすぐに制限されるようです。ノンブロッキングタスクAPIを使用することで、より少ないリソースでより多くの処理を実行できるようになると思います。

私の最初の印象は次のようなものです。

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

しかし、これがボトルネックを引き起こす可能性があることに気づきました。おそらく、HandleConnectionAsyncは、最初の待機に到達するまでに異常に長い時間がかかり、メインの受け入れループの進行を停止します。これはこれまでに1つのスレッドのみを使用しますか、それともランタイムは適切と思われる複数のスレッドで魔法のように実行しますか?

これらの2つのアプローチを組み合わせて、サーバーがアクティブに実行されているタスクの数に必要なスレッドの数を正確に使用するようにする方法はありますが、IOオペレーション?

このような状況でスループットを最大化する慣用的な方法はありますか?

21
captncraig

プロファイリングテストで必要になる可能性がある場合を除いて、フレームワークにスレッドを管理させ、余分なスレッドは作成しません。特に、HandleConnectionAsync内の呼び出しがほとんどIOバウンドである場合。

とにかく、HandleConnectionAsyncの先頭で呼び出しスレッド(ディスパッチャー)を解放したい場合は、非常に簡単な解決策があります。 await Yield()を使用してThreadPoolから新しいスレッドにジャンプできます。これは、サーバーが初期スレッド(コンソール)に同期コンテキストがインストールされていない実行環境で実行されている場合に機能します。 app、WCFサービス)。これは通常、TCPサーバーの場合です。

以下はこれを示しています(コードは元々 ここ からのものです)。メインの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)
                await task;
        }
    }

    // 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 async Task Main(string[] args)
    {
        Console.WriteLine("Hit Ctrl-C to exit.");
        await new Program().StartListener();
    }
}
_

または、コードはawait Task.Yield()なしで次のようになります。 an async lambda to _Task.Run_を渡すことに注意してください。これはHandleConnectionAsync内の非同期APIのメリットを享受し、そこで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 (task.IsFaulted)
                await task;
        }
    });
}
_

このように、StartListener内で作成されたすべての子タスクは、クライアントコードの同期コンテキストの影響を受けません。したがって、どこでも明示的にTask.ConfigureAwait(false)を呼び出す必要はありません。

24
noseratio

既存の回答はTask.Run(() => HandleConnection(client));の使用を正しく提案していますが、その理由は説明されていません。

理由は次のとおりです。HandleConnectionAsyncが最初の待機に到達するまでに時間がかかる可能性があることを懸念しています。 async IO(この場合はそうすべきです)の使用に固執する場合、これはHandleConnectionAsyncがブロッキングなしでCPUバウンド作業を実行していることを意味します。これはスレッドプール:短い、非ブロッキングCPU作業を実行するように作られています。

そして、あなたは正しいです、受け入れループは戻るまでに長い時間がかかるHandleConnectionAsyncによって抑制されるでしょう(おそらくそれにかなりのCPUバウンド作業があるためです)。頻繁に新しい接続が必要な場合は、これを回避する必要があります。

ループを抑制している重要な作業がないことが確実な場合は、追加のスレッドプールTaskを保存して、それを実行しないでください。

または、複数の承認を同時に実行することもできます。 await Serve();を(たとえば)に置き換えます。

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

これにより、スケーラビリティの問題が解消されます。 ここでは、awaitが1つを除くすべてのエラーを飲み込むことに注意してください。

8
usr

試す

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

Microsoft http://msdn.Microsoft.com/en-AU/library/hh524395.aspx#BKMK_VoidReturnType によると、例外をキャッチできないため、戻り値のvoid型は使用しないでください。あなたが指摘したように、「ファイアアンドフォーゲット」タスクが必要なので、私の結論は、常にタスクを返す必要があるということです(マイクロソフトが言ったように)が、次を使用してエラーをキャッチする必要があります。

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");
}
1
Luke

非同期接続を受け入れる必要がある理由はありますか?つまり、クライアント接続を待つことはあなたに何か価値を与えますか?それを行う唯一の理由は、接続を待っている間にサーバーで他の作業が行われているためです。もしあれば、おそらく次のようなことをすることができます:

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

このようにして、acceptingは現在のスレッドを解放し、他の処理を行うためのオプションを残し、処理は新しいスレッドで実行されます。唯一のオーバーヘッドは、クライアントが新しい接続の受け入れに直接戻る前に、クライアントを処理するための新しいスレッドを生成することです。

編集:作成したコードとほぼ同じであることに気づきました。あなたが実際に何を求めているのかをよりよく理解するために、あなたの質問をもう一度読む必要があると思います:S

Edit2:

これらの2つのアプローチを組み合わせて、サーバーがアクティブに実行されているタスクの数に必要なスレッドの数を正確に使用するようにする方法はありますが、IOオペレーション?

私の解決策が実際にこの質問に答えると思います。それは本当に必要ですか?

Edit3:Task.Factory.StartNew()が実際に新しいスレッドを作成するようになりました。

0
EMB