[英]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.