web-dev-qa-db-ja.com

C#で実行するタスクを正しくキューに入れる方法

アイテムの列挙があります(RunData.Demand)、HTTP経由でAPIを呼び出すことを含むいくつかの作業をそれぞれ表します。 foreachをすべて実行し、各反復中にAPIを呼び出すだけで十分です。ただし、各反復には1〜2秒かかるため、2〜3個のスレッドを実行し、それらの間で作業を分割します。これが私がやっていることです:

ThreadPool.SetMaxThreads(2, 5); // Trying to limit the amount of threads
var tasks = RunData.Demand
   .Select(service => Task.Run(async delegate
   {
      var availabilityResponse = await client.QueryAvailability(service);
      // Do some other stuff, not really important
   }));

await Task.WhenAll(tasks);

client.QueryAvailability呼び出しは基本的にHttpClientクラスを使用してAPIを呼び出します:

public async Task<QueryAvailabilityResponse> QueryAvailability(QueryAvailabilityMultidayRequest request)
{
   var response = await client.PostAsJsonAsync("api/queryavailabilitymultiday", request);

   if (response.IsSuccessStatusCode)
   {
      return await response.Content.ReadAsAsync<QueryAvailabilityResponse>();
   }

   throw new HttpException((int) response.StatusCode, response.ReasonPhrase);
}

これはしばらくの間はうまく機能しますが、最終的にはタイムアウトが始まります。 HttpClientタイムアウトを1時間に設定すると、奇妙な内部サーバーエラーが発生し始めます。

私が始めたのは、QueryAvailabilityメソッド内にストップウォッチを設定して、何が起こっているのかを確認することでした。

何が起こっているのかというと、RunData.Demandの1200アイテムすべてが一度に作成され、1200 await client.PostAsJsonAsyncメソッドが呼び出されています。次に、2つのスレッドを使用してタスクをゆっくりとチェックバックしているように見えるので、最後に9または10分間待っているタスクがあります。

これが私が望む行動です:

1,200個のタスクを作成し、スレッドが使用可能になったら一度に3〜4個実行します。私はnot 1,200のHTTPコールをすぐにキューに入れたいです。

これを行うための良い方法はありますか?

20

いつもお勧めしますが、TPL Dataflowが必要です(インストールする場合:Install-Package Microsoft.Tpl.Dataflow)。

各アイテムに対して実行するアクションを含むActionBlockを作成します。スロットリング用にMaxDegreeOfParallelismを設定します。それに投稿を開始し、その完了を待ちます。

var block = new ActionBlock<QueryAvailabilityMultidayRequest>(async service => 
{
    var availabilityResponse = await client.QueryAvailability(service);
    // ...
},
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 4 });

foreach (var service in RunData.Demand)
{
    block.Post(service);
}

block.Complete();
await block.Completion;
21
i3arnon

古い質問ですが、 SemaphoreSlim クラスを使用した代替の軽量ソリューションを提案したいと思います。 System.Threadingを参照するだけです。

SemaphoreSlim sem = new SemaphoreSlim(4,4);

foreach (var service in RunData.Demand)
{

    await sem.WaitAsync();
    Task t = Task.Run(async () => 
    {
        var availabilityResponse = await client.QueryAvailability(serviceCopy));    
        // do your other stuff here with the result of QueryAvailability
    }
    t.ContinueWith(sem.Release());
}

セマフォはロックメカニズムとして機能します。セマフォに入るには、カウントから1を引くWait(WaitAsync)を呼び出す必要があります。 releaseを呼び出すと、カウントが1つ増えます。

5
SeanOB

非同期HTTP呼び出しを使用しているため、スレッド数を制限しても効果がありません(ParallelOptions.MaxDegreeOfParallelism in Parallel.ForEach回答の1つが示唆しているように)。単一のスレッドでも、すべての要求を開始し、到着時に結果を処理できます。

これを解決する1つの方法は、TPL Dataflowを使用することです。

もう1つの素晴らしい解決策は、ソースIEnumerableをパーティションに分割し、各パーティションのアイテムを順次処理することです このブログ投稿

public static Task ForEachAsync<T>(this IEnumerable<T> source, int dop, Func<T, Task> body)
{
    return Task.WhenAll(
        from partition in Partitioner.Create(source).GetPartitions(dop)
        select Task.Run(async delegate
        {
            using (partition)
                while (partition.MoveNext())
                    await body(partition.Current);
        }));
}
3
Jakub Lortz

Dataflowライブラリはすばらしいですが、ブロック構成を使用しない場合は少し重いと思います。以下の拡張メソッドのようなものを使用する傾向があります。

また、Partitionerメソッドとは異なり、これは呼び出しコンテキストで非同期メソッドを実行します。ただし、コードが完全に非同期でない場合、または「高速パス」をとる場合、明示的にスレッドが作成されないため、同期的に実行されます。

public static async Task RunParallelAsync<T>(this IEnumerable<T> items, Func<T, Task> asyncAction, int maxParallel)
{
    var tasks = new List<Task>();

    foreach (var item in items)
    {
        tasks.Add(asyncAction(item));

        if (tasks.Count < maxParallel)
                continue; 

        var notCompleted = tasks.Where(t => !t.IsCompleted).ToList();

        if (notCompleted.Count >= maxParallel)
            await Task.WhenAny(notCompleted);
    }

    await Task.WhenAll(tasks);
}
2
Andrew Hanlon