5

バックグラウンド

1 つの特定のホストからのコンテンツを使用してバッチ HTML ページ処理を実行するコードがあります。を使用して多数 (~400) の同時 HTTP リクエストを作成しようとしますHttpClient。同時接続の最大数はによって制限されていると思われるServicePointManager.DefaultConnectionLimitため、独自の同時実行制限を適用していません。

HttpClientすべての要求をusingに非同期で送信した後、andTask.WhenAllを使用してバッチ操作全体をキャンセルできます。操作の進行状況はユーザー インターフェイスを介して表示でき、ボタンをクリックしてキャンセルを実行できます。CancellationTokenSourceCancellationToken

問題

CancellationTokenSource.Cancel()約 5 ~ 30 秒間ブロックする呼び出し。これにより、ユーザー インターフェイスがフリーズします。メソッドがキャンセル通知用に登録したコードを呼び出しているために、これが発生したと思われます。

私が考えたこと

  1. 同時 HTTP 要求タスクの数を制限します。HttpClientすでに過剰なリクエスト自体をキューに入れているように見えるため、これを回避策と考えています。
  2. CancellationTokenSource.Cancel()非 UI スレッドでメソッド呼び出しを実行する。これはうまくいきませんでした。他のほとんどのタスクが完了するまで、タスクは実際には実行されませんでした。asyncメソッドのバージョンがうまく機能すると思いますが、見つかりませんでした。また、UIスレッドでメソッドを使うのにも向いている印象です。

デモンストレーション

コード

class Program
{
    private const int desiredNumberOfConnections = 418;

    static void Main(string[] args)
    {
        ManyHttpRequestsTest().Wait();

        Console.WriteLine("Finished.");
        Console.ReadKey();
    }

    private static async Task ManyHttpRequestsTest()
    {
        using (var client = new HttpClient())
        using (var cancellationTokenSource = new CancellationTokenSource())
        {
            var requestsCompleted = 0;

            using (var allRequestsStarted = new CountdownEvent(desiredNumberOfConnections))
            {
                Action reportRequestStarted = () => allRequestsStarted.Signal();
                Action reportRequestCompleted = () => Interlocked.Increment(ref requestsCompleted);
                Func<int, Task> getHttpResponse = index => GetHttpResponse(client, cancellationTokenSource.Token, reportRequestStarted, reportRequestCompleted);
                var httpRequestTasks = Enumerable.Range(0, desiredNumberOfConnections).Select(getHttpResponse);

                Console.WriteLine("HTTP requests batch being initiated");
                var httpRequestsTask = Task.WhenAll(httpRequestTasks);

                Console.WriteLine("Starting {0} requests (simultaneous connection limit of {1})", desiredNumberOfConnections, ServicePointManager.DefaultConnectionLimit);
                allRequestsStarted.Wait();

                Cancel(cancellationTokenSource);
                await WaitForRequestsToFinish(httpRequestsTask);
            }

            Console.WriteLine("{0} HTTP requests were completed", requestsCompleted);
        }
    }

    private static void Cancel(CancellationTokenSource cancellationTokenSource)
    {
        Console.Write("Cancelling...");

        var stopwatch = Stopwatch.StartNew();
        cancellationTokenSource.Cancel();
        stopwatch.Stop();

        Console.WriteLine("took {0} seconds", stopwatch.Elapsed.TotalSeconds);
    }

    private static async Task WaitForRequestsToFinish(Task httpRequestsTask)
    {
        Console.WriteLine("Waiting for HTTP requests to finish");

        try
        {
            await httpRequestsTask;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("HTTP requests were cancelled");
        }
    }

    private static async Task GetHttpResponse(HttpClient client, CancellationToken cancellationToken, Action reportStarted, Action reportFinished)
    {
        var getResponse = client.GetAsync("http://www.google.com", cancellationToken);

        reportStarted();
        using (var response = await getResponse)
            response.EnsureSuccessStatusCode();
        reportFinished();
    }
}

出力

キャンセルが 13 秒以上ブロックされたことを示すコンソール ウィンドウ

キャンセルが長時間ブロックされるのはなぜですか? また、私が間違っていること、または改善できることはありますか?

4

1 に答える 1

5

非 UI スレッドで CancellationTokenSource.Cancel() メソッド呼び出しを実行します。これはうまくいきませんでした。他のほとんどのタスクが完了するまで、タスクは実際には実行されませんでした。

これが私に教えてくれるのは、おそらく「スレッドプールの枯渇」に苦しんでいるということです。これは、スレッドプールキューに(HTTP リクエストの完了から)非常に多くのアイテムがあり、それらすべてを処理するのに時間がかかる場所です。実行中のスレッドプール作業項目でキャンセルがブロックされている可能性があり、キューの先頭にスキップできません。

これは、考慮リストのオプション 1 を使用する必要があることを示唆しています。スレッドプール キューが比較的短くなるように、自分の作業を抑制します。とにかく、これはアプリの応答性全体に適しています。

非同期作業を調整する私のお気に入りの方法は、Dataflowを使用することです。このようなもの:

var block = new ActionBlock<Uri>(
    async uri => {
        var httpClient = new HttpClient(); // HttpClient isn't thread-safe, so protect against concurrency by using a dedicated instance for each request.
        var result = await httpClient.GetAsync(uri);
        // do more stuff with result.
    },
    new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 20, CancellationToken = cancellationToken });
for (int i = 0; i < 1000; i++)
    block.Post(new Uri("http://www.server.com/req" + i));
block.Complete();
await block.Completion; // waits until everything is done or canceled.

別の方法として、TaskCreationOptions.LongRunning を渡す Task.Factory.StartNew を使用して、タスクが新しいスレッド (threadpool に関連付けられていない) を取得し、すぐに開始してそこから Cancel を呼び出すことができます。ただし、代わりにスレッドプールの枯渇の問題を解決する必要があります。

于 2013-02-17T15:49:52.703 に答える