同時非同期I / O操作の量を制限するにはどうすればよいですか?


115
// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
urls.AsParallel().ForAll(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
});

これが問題です。1000以上の同時Webリクエストを開始します。これらの非同期HTTPリクエストの同時量を制限する簡単な方法はありますか?そのため、一度にダウンロードされるWebページは20を超えません。最も効率的な方法でそれを行う方法?


2
これは前の質問とどう違うのですか?
スビック


4
@ChrisDisley、これはリクエストの起動のみを並列化します。
消費者

@svickは正しいですが、どう違うのですか?ところで、私はそこの答えが大好きですstackoverflow.com/a/10802883/66372
eglasius 14年

3
ほかにHttpClientありIDisposable、そしてあなたがそれらの1000 +を使用するつもりだ場合は特に、それを配置する必要があります。HttpClient複数のリクエストのシングルトンとして使用できます。
Shimmy Weitzhandler 2015

回答:


161

.NET 4.5 Betaを使用して、async for .NETの最新バージョンでこれを確実に行うことができます。以前の「usr」からの投稿は、Stephen Toubによって書かれた優れた記事を指していますが、あまり発表されていないニュースは、非同期セマフォが実際に.NET 4.5のベータリリースに組み込まれたことです。

私たちの最愛のSemaphoreSlimクラス(元のクラスよりもパフォーマンスが高いので使用する必要がありますSemaphore)を見るWaitAsync(...)と、予想されるすべての引数(タイムアウト間隔、キャンセルトークン、通常のスケジュール設定のすべての友達)を備えた一連のオーバーロードを誇っています。 )

また、Stephenは、ベータ版でリリースされた新しい.NET 4.5の優れた機能に関する最新のブログ投稿を執筆しています。「What's New for Parallelism in .NET 4.5 Beta」を参照してください。

最後に、非同期メソッドのスロットルにSemaphoreSlimを使用する方法に関するサンプルコードをいくつか示します。

public async Task MyOuterMethod()
{
    // let's say there is a list of 1000+ URLs
    var urls = { "http://google.com", "http://yahoo.com", ... };

    // now let's send HTTP requests to each of these URLs in parallel
    var allTasks = new List<Task>();
    var throttler = new SemaphoreSlim(initialCount: 20);
    foreach (var url in urls)
    {
        // do an async wait until we can schedule again
        await throttler.WaitAsync();

        // using Task.Run(...) to run the lambda in its own parallel
        // flow on the threadpool
        allTasks.Add(
            Task.Run(async () =>
            {
                try
                {
                    var client = new HttpClient();
                    var html = await client.GetStringAsync(url);
                }
                finally
                {
                    throttler.Release();
                }
            }));
    }

    // won't get here until all urls have been put into tasks
    await Task.WhenAll(allTasks);

    // won't get here until all tasks have completed in some way
    // (either success or exception)
}

最後に、おそらく価値のある言及は、TPLベースのスケジューリングを使用するソリューションです。TPLでまだ開始されていないデリゲートバインドタスクを作成し、カスタムタスクスケジューラで同時実行を制限できます。実際、そのためのMSDNサンプルがあります。

TaskSchedulerも参照してください。


3
並列度が制限されたparallel.foreachは、より優れたアプローチではありませんか?msdn.microsoft.com/en-us/library/...
GreyCloud

2
処分してみませんかHttpClient
Shimmy Weitzhandler

4
@GreyCloud:Parallel.ForEach同期コードで動作します。これにより、非同期コードを呼び出すことができます。
Josh Noe

2
@TheMonarch あなたは間違っている。その上、すべてIDisposableのをusingor try-finallyステートメントで囲み、それらの破棄を保証することは常に良い習慣です。
Shimmy Weitzhandler 2017

29
この回答がどれほど人気が​​あるかを考えると、HttpClientは、リクエストごとのインスタンスではなく、単一の共通インスタンスである可能性があり、またそうである必要があることを指摘する価値があります。
Rupert Rawnsley 2017年

15

IEnumerable(つまり、URLの文字列)があり、これらのそれぞれでI / Oバウンド操作を実行する(つまり、非同期のhttp要求を行う)必要がある場合、およびオプションで、同時の最大数を設定する場合リアルタイムのI / O要求。これを行う方法を次に示します。この方法では、スレッドプールなどを使用せず、メソッドはsemaphoreslimを使用して、1つの要求が完了するスライディングウィンドウパターンと同様に最大同時I / O要求を制御し、セマフォを残して、次の要求を取得します。

使用方法:ForEachAsync(urlStrings、YourAsyncFunc、optionalMaxDegreeOfConcurrency);

public static Task ForEachAsync<TIn>(
        IEnumerable<TIn> inputEnumerable,
        Func<TIn, Task> asyncProcessor,
        int? maxDegreeOfParallelism = null)
    {
        int maxAsyncThreadCount = maxDegreeOfParallelism ?? DefaultMaxDegreeOfParallelism;
        SemaphoreSlim throttler = new SemaphoreSlim(maxAsyncThreadCount, maxAsyncThreadCount);

        IEnumerable<Task> tasks = inputEnumerable.Select(async input =>
        {
            await throttler.WaitAsync().ConfigureAwait(false);
            try
            {
                await asyncProcessor(input).ConfigureAwait(false);
            }
            finally
            {
                throttler.Release();
            }
        });

        return Task.WhenAll(tasks);
    }


この実装と使用法では、SemaphoreSlimを明示的に破棄する必要はありません。SemaphoreSlimはメソッド内で内部的に使用され、メソッドはAvailableWaitHandleプロパティにアクセスしないため、usingブロック内で破棄またはラップする必要があります。
Dogu Arslan

1
私たちが他の人に教えるベストプラクティスとレッスンを考えるだけ Aはusingいいだろう。
AgentFire

よく私はこの例に従うことができますが、これを行うための最良の方法は何かを試してみて、基本的にスロットルを持っていますが、私のFuncはリストを返します。リストにロックする必要があります。提案はありますか?
Seabizkit

メソッドを少し更新して実際のタスクのリストを返し、呼び出しコード内からTask.WhenAllを待つことができます。Task.WhenAllが完了すると、リスト内の各タスクを列挙し、そのリストを最終リストに追加できます。メソッドシグネチャを「public static IEnumerable <Task <TOut >> ForEachAsync <TIn、TOut>(IEnumerable <TIn> inputEnumerable、Func <TIn、Task <TOut >> asyncProcessor、int?maxDegreeOfParallelism = null)」に変更します
Arslan

7

残念ながら、.NET Frameworkには、並列非同期タスクを調整するための最も重要なコンビネーターがありません。そのようなものは組み込まれていません。

最も立派なStephen Toubによって構築されたAsyncSemaphoreクラスを見てください。必要なのはセマフォと呼ばれ、非同期バージョンが必要です。


12
「残念ながら、.NET Frameworkには、並列非同期タスクを調整するための最も重要なコンビネーターがありません。そのようなものは組み込まれていません。」.NET 4.5 Beta以降、は正しくありません。SemaphoreSlimがWaitAsync(...)機能を提供するようになりました:)
Theo Yaung

SemaphoreSlim(新しいasyncメソッドを含む)はAsyncSemphoreよりも優先されますか、それともToubの実装にはまだ利点がありますか?
Todd Menier 2013

私の意見では、組み込み型は、十分にテストおよび設計されている可能性が高いため、推奨されます。
usr

4
スティーブンは彼のブログの投稿への質問に対するコメントにコメントを追加し、.NET 4.5にSemaphoreSlimを使用するのが一般的な方法であることを確認しました。
jdasilva 2013年

7

多くの落とし穴があり、セマフォを直接使用することはエラーの場合にトリッキーになる可能性があるため、ホイールを再発明する代わりにAsyncEnumerator NuGetパッケージを使用することをお勧めします。

// let's say there is a list of 1000+ URLs
string[] urls = { "http://google.com", "http://yahoo.com", ... };

// now let's send HTTP requests to each of these URLs in parallel
await urls.ParallelForEachAsync(async (url) => {
    var client = new HttpClient();
    var html = await client.GetStringAsync(url);
}, maxDegreeOfParalellism: 20);

4

Theo Yaungの例はいいですが、待機中のタスクのリストがないバリアントがあります。

 class SomeChecker
 {
    private const int ThreadCount=20;
    private CountdownEvent _countdownEvent;
    private SemaphoreSlim _throttler;

    public Task Check(IList<string> urls)
    {
        _countdownEvent = new CountdownEvent(urls.Count);
        _throttler = new SemaphoreSlim(ThreadCount); 

        return Task.Run( // prevent UI thread lock
            async  () =>{
                foreach (var url in urls)
                {
                    // do an async wait until we can schedule again
                    await _throttler.WaitAsync();
                    ProccessUrl(url); // NOT await
                }
                //instead of await Task.WhenAll(allTasks);
                _countdownEvent.Wait();
            });
    }

    private async Task ProccessUrl(string url)
    {
        try
        {
            var page = await new WebClient()
                       .DownloadStringTaskAsync(new Uri(url)); 
            ProccessResult(page);
        }
        finally
        {
            _throttler.Release();
            _countdownEvent.Signal();
        }
    }

    private void ProccessResult(string page){/*....*/}
}

4
このアプローチを使用すると危険が1つあることに注意してください。発生する例外ProccessUrlやそのサブ関数は実際には無視されます。それらはTasksにキャプチャされますが、元のの呼び出し元には浸透しませんCheck(...)。個人的には、私がTasksとそのコンビネーター関数をWhenAllandのように使用して、WhenAnyエラー伝播を改善する理由です。:)
Theo Yaung 2016年

3

SemaphoreSlimは、ここで非常に役立ちます。これが私が作成した拡張メソッドです。

    /// <summary>
    /// Concurrently Executes async actions for each item of <see cref="IEnumerable<typeparamref name="T"/>
    /// </summary>
    /// <typeparam name="T">Type of IEnumerable</typeparam>
    /// <param name="enumerable">instance of <see cref="IEnumerable<typeparamref name="T"/>"/></param>
    /// <param name="action">an async <see cref="Action" /> to execute</param>
    /// <param name="maxActionsToRunInParallel">Optional, max numbers of the actions to run in parallel,
    /// Must be grater than 0</param>
    /// <returns>A Task representing an async operation</returns>
    /// <exception cref="ArgumentOutOfRangeException">If the maxActionsToRunInParallel is less than 1</exception>
    public static async Task ForEachAsyncConcurrent<T>(
        this IEnumerable<T> enumerable,
        Func<T, Task> action,
        int? maxActionsToRunInParallel = null)
    {
        if (maxActionsToRunInParallel.HasValue)
        {
            using (var semaphoreSlim = new SemaphoreSlim(
                maxActionsToRunInParallel.Value, maxActionsToRunInParallel.Value))
            {
                var tasksWithThrottler = new List<Task>();

                foreach (var item in enumerable)
                {
                    // Increment the number of currently running tasks and wait if they are more than limit.
                    await semaphoreSlim.WaitAsync();

                    tasksWithThrottler.Add(Task.Run(async () =>
                    {
                        await action(item).ContinueWith(res =>
                        {
                            // action is completed, so decrement the number of currently running tasks
                            semaphoreSlim.Release();
                        });
                    }));
                }

                // Wait for all of the provided tasks to complete.
                await Task.WhenAll(tasksWithThrottler.ToArray());
            }
        }
        else
        {
            await Task.WhenAll(enumerable.Select(item => action(item)));
        }
    }

使用例:

await enumerable.ForEachAsyncConcurrent(
    async item =>
    {
        await SomeAsyncMethod(item);
    },
    5);

0

古い質問、新しい答え。@vitidevには、私がレビューしたプロジェクトでほとんどそのまま再利用されたコードのブロックがありました。数人の同僚と話し合った後、「なぜ組み込みのTPLメソッドを使用しないのですか?」ActionBlockは勝者のようです。 https://msdn.microsoft.com/en-us/library/hh194773(v=vs.110).aspx。おそらく、既存のコードを変更することはありませんが、このナゲットを採用し、スロットル並列処理に関するSofty氏のベストプラクティスを再利用することは間違いありません。


0

これは、LINQのレイジーな性質を利用するソリューションです。機能的には受け入れられた回答)と同等ですが、の代わりにworker-tasksを使用するためSemaphoreSlim、この方法で操作全体のメモリフットプリントが削減されます。最初に、スロットリングなしで機能させます。最初のステップは、URLを列挙可能なタスクに変換することです。

string[] urls =
{
    "https://stackoverflow.com",
    "https://superuser.com",
    "https://serverfault.com",
    "https://meta.stackexchange.com",
    // ...
};
var httpClient = new HttpClient();
var tasks = urls.Select(async (url) =>
{
    return (Url: url, Html: await httpClient.GetStringAsync(url));
});

次のステップは awaitTask.WhenAllメソッドを使用すべてのタスクを同時に実行することです。

var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
    Console.WriteLine($"Url: {result.Url}, {result.Html.Length:#,0} chars");
}

出力:

Url:https : //stackoverflow.com、105.574文字
Url:https : //superuser.com、126.953文字
Url:https : //serverfault.com、125.963文字
Url: https://meta.stackexchange.com、185.276文字
...

Microsoftの実装ではTask.WhenAll、提供された列挙可能な配列を即座に具体化し、すべてのタスクを一度に開始します。同時の非同期操作の数を制限したいので、これは望ましくありません。したがって、代替を実装する必要がありますWhenAll列挙可能なものをゆっくりとゆっくりと列挙する。これを行うには、多数のワーカータスク(必要な同時実行レベルに等しい)を作成します。各ワーカータスクは、一度に列挙可能な1つのタスクを列挙し、ロックを使用して各urlタスクが確実に処理されるようにします。 1つのワーカータスクのみ。次にawait、すべてのワーカータスクを完了し、最後に結果を返します。ここに実装があります:

public static async Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks,
    int concurrencyLevel)
{
    if (tasks is ICollection<Task<T>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    var locker = new object();
    var results = new List<T>();
    var failed = false;
    using (var enumerator = tasks.GetEnumerator())
    {
        var workerTasks = Enumerable.Range(0, concurrencyLevel)
        .Select(async _ =>
        {
            try
            {
                while (true)
                {
                    Task<T> task;
                    int index;
                    lock (locker)
                    {
                        if (failed) break;
                        if (!enumerator.MoveNext()) break;
                        task = enumerator.Current;
                        index = results.Count;
                        results.Add(default); // Reserve space in the list
                    }
                    var result = await task.ConfigureAwait(false);
                    lock (locker) results[index] = result;
                }
            }
            catch (Exception)
            {
                lock (locker) failed = true;
                throw;
            }
        }).ToArray();
        await Task.WhenAll(workerTasks).ConfigureAwait(false);
    }
    lock (locker) return results.ToArray();
}

...そして、これが、望ましいコードを実現するために、最初のコードで変更しなければならないものです。

var results = await WhenAll(tasks, concurrencyLevel: 2);

例外の処理に関して違いがあります。ネイティブはTask.WhenAll、すべてのタスクが完了するまで待機し、すべての例外を集約します。上記の実装は、最初の障害が発生したタスクが完了するとすぐに終了します。



-1

1000個のタスクが非常に速くキューに入れられる可能性がありますが、Parallel Tasksライブラリは、マシンのCPUコアの量に等しい同時タスクのみを処理できます。つまり、4コアマシンの場合、(MaxDegreeOfParallelismを下げない限り)一度に実行されるタスクは4つだけです。


8
うん、でもそれは非同期I / O操作とは関係ありません。上記のコードは、シングルスレッドで実行されている場合でも、1000以上の同時ダウンロードを起動します。
Grief Coder

awaitそこにキーワードが見つかりませんでした。それを取り除くと問題が解決するはずですよね?
scottm

2
ライブラリRunningは、コアの数よりも多くのタスクを同時に実行できます(ステータスと共に)。これは、特にI / Oバウンドタスクの場合です。
スビック

@svick:うん。(スレッドではなく)最大同時TPLタスクを効率的に制御する方法を知っていますか?
Grief Coder、2012年

-1

CPUバウンド操作を高速化するには、並列計算を使用する必要があります。ここでは、I / Oバウンド操作について説明しています。あなたの実装は純粋に非同期でマルチコアCPUのビジーなシングルコアを圧倒しない限り、であるます。

編集 ここでは、「非同期セマフォ」を使用するというusrの提案が気に入っています。


いい視点ね!ただし、ここでの各タスクには非同期コードと同期コードが含まれます(ページは非同期にダウンロードされ、同期して処理されます)。CPU全体にコードの同期部分を分散させ、同時に非同期I / O操作の量を制限しようとしています。
Grief Coder

どうして?1000以上のHTTPリクエストを同時に起動することは、ユーザーのネットワーク容量に適したタスクではない可能性があるためです。
スペンダー

並列拡張は、純粋な非同期ソリューションを手動で実装することなく、I / O操作を多重化する方法としても使用できます。私が同意するのはずさんだと考えることもできますが、並行操作の数を厳しく制限している限り、スレッドプールに過度の負担をかけることはないでしょう。
Sean U

3
この答えが答えを提供するとは思わない。純粋に非同期であるだけでは十分ではありません。物理的なIOをノンブロッキングで抑制したいのです。
usr

1
うーん、確かに同意しません...大規模なプロジェクトで作業する場合、あまりに多くの開発者がこの見方をする場合、各開発者の貢献が単独では不十分であっても、飢餓状態になります。ThreadPool が1つしかないことを考えると、たとえそれを半敬意を持って扱っていても...他のすべての人が同じことをしている場合、問題が発生する可能性があります。そのため、私は常に ThreadPoolで長いものを実行しないようにアドバイスしています。
消費者

-1

MaxDegreeOfParallelism指定できるオプションであるを使用しますParallel.ForEach()

var options = new ParallelOptions { MaxDegreeOfParallelism = 20 };

Parallel.ForEach(urls, options,
    url =>
        {
            var client = new HttpClient();
            var html = client.GetStringAsync(url);
            // do stuff with html
        });

4
私はこれがうまくいくとは思いません。 GetStringAsync(url)で呼び出されることを意図していますawait。あなたがの種類を検査した場合var html、それはTask<string>、ない結果string
Neal Ehardt、2015

2
@NealEhardtは正しいです。同期コードのParallel.ForEach(...)ブロックを並列に実行することを目的としています(たとえば、異なるスレッドで)。
Theo Yaung、2016年

-1

基本的に、ヒットする各URLに対してアクションまたはタスクを作成し、それらをリストに入れてから、そのリストを処理して、並行して処理できる数を制限します。

私のブログ投稿では、タスクとアクションの両方でこれを行う方法を示し、ダウンロードして実行して両方の動作を確認できるサンプルプロジェクトを提供しています。

アクションあり

アクションを使用する場合は、組み込みの.Net Parallel.Invoke関数を使用できます。ここでは、最大20スレッドの並列実行に制限しています。

var listOfActions = new List<Action>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(() => CallUrl(localUrl)));
}

var options = new ParallelOptions {MaxDegreeOfParallelism = 20};
Parallel.Invoke(options, listOfActions.ToArray());

タスクあり

タスクには組み込み関数はありません。ただし、ブログで提供しているものを使用できます。

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run, at most, the specified number of tasks in parallel.
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, CancellationToken cancellationToken = new CancellationToken())
    {
        await StartAndWaitAllThrottledAsync(tasksToRun, maxTasksToRunInParallel, -1, cancellationToken);
    }

    /// <summary>
    /// Starts the given tasks and waits for them to complete. This will run the specified number of tasks in parallel.
    /// <para>NOTE: If a timeout is reached before the Task completes, another Task may be started, potentially running more than the specified maximum allowed.</para>
    /// <para>NOTE: If one of the given tasks has already been started, an exception will be thrown.</para>
    /// </summary>
    /// <param name="tasksToRun">The tasks to run.</param>
    /// <param name="maxTasksToRunInParallel">The maximum number of tasks to run in parallel.</param>
    /// <param name="timeoutInMilliseconds">The maximum milliseconds we should allow the max tasks to run in parallel before allowing another task to start. Specify -1 to wait indefinitely.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    public static async Task StartAndWaitAllThrottledAsync(IEnumerable<Task> tasksToRun, int maxTasksToRunInParallel, int timeoutInMilliseconds, CancellationToken cancellationToken = new CancellationToken())
    {
        // Convert to a list of tasks so that we don't enumerate over it multiple times needlessly.
        var tasks = tasksToRun.ToList();

        using (var throttler = new SemaphoreSlim(maxTasksToRunInParallel))
        {
            var postTaskTasks = new List<Task>();

            // Have each task notify the throttler when it completes so that it decrements the number of tasks currently running.
            tasks.ForEach(t => postTaskTasks.Add(t.ContinueWith(tsk => throttler.Release())));

            // Start running each task.
            foreach (var task in tasks)
            {
                // Increment the number of tasks currently running and wait if too many are running.
                await throttler.WaitAsync(timeoutInMilliseconds, cancellationToken);

                cancellationToken.ThrowIfCancellationRequested();
                task.Start();
            }

            // Wait for all of the provided tasks to complete.
            // We wait on the list of "post" tasks instead of the original tasks, otherwise there is a potential race condition where the throttler's using block is exited before some Tasks have had their "post" action completed, which references the throttler, resulting in an exception due to accessing a disposed object.
            await Task.WhenAll(postTaskTasks.ToArray());
        }
    }

次に、タスクのリストを作成し、関数を呼び出してタスクを実行します。たとえば、一度に最大20を同時に実行すると、次のようになります。

var listOfTasks = new List<Task>();
foreach (var url in urls)
{
    var localUrl = url;
    // Note that we create the Task here, but do not start it.
    listOfTasks.Add(new Task(async () => await CallUrl(localUrl)));
}
await Tasks.StartAndWaitAllThrottledAsync(listOfTasks, 20);

SemaphoreSlimのinitialCountを指定しているだけで、SemaphoreSlimのコンストラクターで2番目のパラメーター、つまりmaxCountを指定する必要があると思います。
ジェイシャー

各タスクからの各応答を処理してリストにします。返却結果または応答を取得するにはどうすればよいですか
venkat

-1

グローバル変数を変更するため、これは良い方法ではありません。また、非同期の一般的なソリューションでもありません。しかし、それだけで十分であれば、HttpClientのすべてのインスタンスにとって簡単です。あなたは単に試すことができます:

System.Net.ServicePointManager.DefaultConnectionLimit = 20;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.