簡體   English   中英

如何正確排隊任務以在 C# 中運行

[英]How to correctly queue up tasks to run in C#

我有一個項目枚舉( RunData.Demand ),每個項目都代表一些涉及通過 HTTP 調用 API 的工作。 如果我只是foreach所有內容並在每次迭代期間調用 API,則效果很好。 但是,每次迭代需要一兩秒鍾,所以我想運行 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 超時設置為一個小時,那么我開始收到奇怪的內部服務器錯誤。

我開始做的是在QueryAvailability方法中設置一個秒表來查看發生了什么。

發生的事情是 RunData.Demand 中的所有 1200 個項目都被同時創建,並且所有 1200 個await client.PostAsJsonAsync方法都被調用。 看起來它然后使用 2 個線程來慢慢檢查任務,所以最后我有等待 9 或 10 分鍾的任務。

這是我想要的行為:

我想創建 1,200 個任務,然后在線程可用時一次運行 3-4 個。 不想排隊1200 HTTP立即調用。

有什么好的方法可以做到這一點嗎?

正如我一直建議的那樣......你需要的是 TPL Dataflow(安裝: Install-Package System.Threading.Tasks.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;

老問題,但我想提出一個使用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 將計數加一。

您正在使用異步 HTTP 調用,因此限制線程數將無濟於事(正如答案之一所示, ParallelOptions.MaxDegreeOfParallelism中的Parallel.ForEach也無濟於事)。 即使是單個線程也可以發起所有請求並在結果到達時對其進行處理。

解決它的一種方法是使用 TPL Dataflow。

另一個不錯的解決方案是將源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);
        }));
}

雖然 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);
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM