web-dev-qa-db-ja.com

同時非同期I / O操作の量を制限する方法は?

// 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リクエストの同時量を制限する簡単な方法はありますか?そのため、常に20を超えるWebページはダウンロードされません。最も効率的な方法でそれを行うには?

95
Grief Coder

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

最愛の SemaphoreSlim クラス(元の Semaphore )、今では WaitAsync(...) シリーズを誇っています予想されるすべての引数-タイムアウト間隔、キャンセルトークン、すべての通常のスケジューリングフレンド:とのオーバーロードの

Stephenは、ベータ版で出た新しい.NET 4.5の利点に関する最新のブログ記事も書きました。 。NET 4.5ベータ版の並列処理の新機能 を参照してください。

最後に、非同期メソッドの調整に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 も参照してください。

139
Theo Yaung

IEnumerable(つまり、URLの文字列)があり、これらのそれぞれでI/Oバウンド操作を実行する(つまり、非同期HTTP要求を作成する)場合、およびオプションで、同時実行の最大数を設定する場合リアルタイムでのI/O要求。これを行う方法を次に示します。この方法では、スレッドプールなどを使用せず、メソッドはセマフォリムを使用して、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);
    }
10
Dogu Arslan

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

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

8
usr

多くの落とし穴があり、エラーの場合にセマフォを直接使用するのは難しい場合があるため、ホイールを再発明する代わりに AsyncEnumerator NuGet Package を使用することをお勧めします。

// 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);
5
Serge Semenov

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
vitidev

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);
2
Jay Shah

https://stackoverflow.com/a/10810730/1186165 のより簡潔なバージョン

static async Task WhenAll(IEnumerable<Task> tasks, int maxThreadCount) {
    using (var guard = new SemaphoreSlim(initialCount: maxThreadCount)) {
        await Task.WhenAll(tasks.Select(async task => {
            await guard.WaitAsync();

            return task.ContinueWith(t => guard.Release());
        }));
    }
}
2
Kittoes0124

これは、LINQの遅延性を活用するソリューションです。 受け入れられた答え のような)スレッドを生成せず、すべてのタスクを一度に作成せず、それらのほとんどすべてをSemaphoreSlimソリューションのように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));
});

2番目のステップは、 Task.WhenAll メソッドを使用して、awaitすべてのタスクを同時に実行することです。

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 chars
URL: https://serverfault.com 、125.963文字
URL: https://meta.stackexchange.com 、185.276文字
...

Microsoftの実装 of Task.WhenAllは、指定された列挙型を即座に配列に具体化し、すべてのタスクを一度に開始します。同時非同期操作の数を制限するため、これは望ましくありません。そのため、列挙型を穏やかにゆっくりと列挙する別のWhenAllを実装する必要があります。これを行うには、多数のワーカータスク(目的の並列度に等しい)を作成し、各ワーカータスクは、ロックを使用して列挙可能な1つのタスクを一度に列挙し、各urlタスクが処理されるようにします1つのワーカータスクのみ。次に、すべてのワーカータスクが完了するまでawait、最後に順序を復元した後に結果を返します。実装は次のとおりです。

public static async Task<T[]> WhenAll<T>(IEnumerable<Task<T>> tasks,
    int degreeOfParallelism)
{
    if (tasks is ICollection<Task<T>>) throw new ArgumentException(
        "The enumerable should not be materialized.", nameof(tasks));
    var results = new List<(int Index, T Result)>();
    var failed = false;
    using (var enumerator = tasks.GetEnumerator())
    {
        int index = 0;
        var workerTasks = Enumerable.Range(0, degreeOfParallelism)
        .Select(async _ =>
        {
            try
            {
                while (true)
                {
                    Task<T> task;
                    int localIndex;
                    lock (enumerator)
                    {
                        if (failed || !enumerator.MoveNext()) break;
                        task = enumerator.Current;
                        localIndex = index++;
                    }
                    var result = await task.ConfigureAwait(false);
                    lock (results) results.Add((localIndex, result));
                }
            }
            catch
            {
                lock (enumerator) failed = true;
                throw;
            }
        }).ToArray();
        await Task.WhenAll(workerTasks).ConfigureAwait(false);
    }
    return results.OrderBy(e => e.Index).Select(e => e.Result).ToArray();
}

...そして、望ましいコードを達成するために、初期コードを変更する必要があります:

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

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

0
Theodor Zoulias

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
        });
0
Sean U

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

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

0
scottm

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

[〜#〜] edit [〜#〜]ここで「非同期セマフォ」を使用するというusrの提案が好きです。

0
GregC