// 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ページはダウンロードされません。最も効率的な方法でそれを行うには?
.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 も参照してください。
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);
}
残念ながら、.NET Frameworkには、並列非同期タスクを調整するための最も重要なコンビネーターがありません。そのようなものは組み込まれていません。
最も尊敬されるStephen Toubによって構築された AsyncSemaphore クラスを見てください。必要なものはセマフォと呼ばれ、非同期バージョンが必要です。
多くの落とし穴があり、エラーの場合にセマフォを直接使用するのは難しい場合があるため、ホイールを再発明する代わりに 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);
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){/*....*/}
}
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);
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());
}));
}
}
これは、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
は、すべてのタスクが完了するまで待機し、すべての例外を集約します。上記の実装は、最初にエラーが発生したタスクの完了後すぐに待機を停止します。
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
});
古い質問、新しい答え。 @vitidevには、レビューしたプロジェクトでほぼそのまま再利用されるコードブロックがありました。数人の同僚と話し合った後、「組み込みのTPLメソッドを使用しないのはなぜですか?」 ActionBlockはそこに勝者のように見えます。 https://msdn.Microsoft.com/en-us/library/hh194773(v = vs.110).aspx おそらく既存のコードを変更することはないでしょうが、間違いなくこの核を採用し、スロットル並列化のためのミスター・ソフティのベストプラクティスを再利用しようとするでしょう。
1000個のタスクが非常に高速にキューに入れられる可能性がありますが、Parallel Tasksライブラリは、マシンのCPUコアの量に等しい同時タスクのみを処理できます。つまり、4コアマシンを使用している場合、特定の時間に実行されるタスクは4つだけです(MaxDegreeOfParallelismを下げない限り)。
CPUバウンド操作を高速化するには、並列計算を使用する必要があります。ここでは、I/Oバウンド操作について説明しています。マルチコアCPUで忙しいシングルコアを圧倒している場合を除き、実装は 純粋に非同期 である必要があります。
[〜#〜] edit [〜#〜]ここで「非同期セマフォ」を使用するというusrの提案が好きです。