[英]C# AsyncEnumerable running/awaiting multiple tasks never finishes
我想要一個接收Task<bool>
並在X任務中運行的函數。
為此,我編寫了以下代碼:
public static class RetryComponent
{
public static async Task RunTasks(Func<Task<bool>> action, int tasks, int retries, string method)
{
// Running everything
var tasksPool = Enumerable.Range(0, tasks).Select(i => DoWithRetries(action, retries, method)).ToArray();
await Task.WhenAll(tasksPool);
}
private static async Task<bool> DoWithRetries(Func<Task<bool>> action, int retryCount, string method)
{
while (true)
{
if (retryCount <= 0)
return false;
try
{
bool res = await action();
if (res)
return true;
}
catch (Exception e)
{
// Log it
}
retryCount--;
await Task.Delay(200); // retry in 200
}
}
}
和以下執行代碼:
BlockingCollection<int> ints = new BlockingCollection<int>();
foreach (int i in Enumerable.Range(0, 100000))
{
ints.Add(i);
}
ints.CompleteAdding();
int taskId = 0;
var enumerable = new AsyncEnumerable<int>(async yield =>
{
await RetryComponent.RunTasks(async () =>
{
try
{
int myTaskId = Interlocked.Increment(ref taskId);
// usually there are async/await operations inside the while loop, this is just an example
while (!ints.IsCompleted)
{
int number = ints.Take();
Console.WriteLine($"Task {myTaskId}: {number}");
await yield.ReturnAsync(number);
}
}
catch (InvalidOperationException)
{
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
return true;
}, 10, 1, MethodBase.GetCurrentMethod().Name);
});
await enumerable.ForEachAsync(number =>
{
Console.WriteLine(number);
});
其中AsyncEnumerable
來自System.Collections.Async
。
控制台顯示任務10:X(其中x是列表中的數字..)。
當我刪除AsyncEnumerable
一切都按預期工作(所有任務都在打印中並且執行結束)。由於某種原因(我找不到很長時間),使用AsyncEnumerable
破壞一切(在我的主代碼中,我需要它來使用AsyncEnumerable
..可伸縮性內容..)意味着代碼永遠不會停止,只有最后一個任務(10)正在打印。 當我添加更多日志時,我看到任務1-9永遠不會完成。
因此,為了弄清楚事情,我想讓多個任務執行異步操作,並將結果產生給充當管道的單個AsyncEnumerable對象。 (這就是主意。)
問題在於枚舉器/生成器模式是順序的,但是您正在嘗試創建多生產者,單消費者模式。 由於您使用嵌套的匿名函數,並且堆棧溢出不會顯示行號,因此很難准確描述我要指代的代碼的哪一部分,但是無論如何我都會嘗試。
AsyncEnumerable的工作方式基本上是等待生產者產生一個值,然后等待使用者使用該值,然后重復。 它不支持生產者和消費者以不同的速度運行,因此為什么我說這種模式是連續的。 它沒有生產項目的隊列, 只有當前值 。 ReturnAsync不等待使用者使用該值,而是應該等待它返回的任務,這會向您發出信號,表明已准備就緒。 因此,我們可以得出結論,它不是線程安全的。
但是, RetryComponent.RunTasks
並行運行10個任務,該代碼調用yield.ReturnAsync
而不檢查是否有人已經調用它,以及是否已經完成該任務。 由於Yield類僅存儲當前值,因此您的10個並發任務會覆蓋當前值,而無需等待Yield
對象准備好新值,因此9個任務會丟失並且永遠不會等待。 由於這9個任務從未等待,因此方法永遠不會完成,而Task.WhenAll
永遠不會返回,並且整個調用堆棧中的任何其他方法也不會執行。
我在github上創建了一個問題,提議他們改進其庫以在發生這種情況時引發異常。 如果他們實現了,則catch塊會將消息寫入控制台並重新拋出錯誤,使任務處於故障狀態,這將允許task.WhenAll
完成,因此程序不會掛起。
您可以使用多線程同步API來確保一次僅調用一項任務yield.ReturnAsync
並等待返回任務。 或者您可以避免使用多生產者模式,因為單個生產者可以輕松地成為枚舉器。 否則,您將需要完全重新考慮如何實現多生產者模式。 我建議TPL Dataflow內置於.NET Core中,並作為NuGet包在.NET Framework中提供。
@zivkan關於順序生產者模式絕對正確。 如果要為單個流擁有並發生產者,仍然可以使用AsyncEnumerable庫來實現,但是需要一些額外的代碼。
這是並發的生產者和使用者(在這種情況下,只有一個使用者)的問題解決方案示例:
static void Main(string[] args)
{
var e = new AsyncEnumerable<int>(async yield =>
{
var threadCount = 10;
var maxItemsOnQueue = 20;
var queue = new ConcurrentQueue<int>();
var consumerLimiter = new SemaphoreSlim(initialCount: 0, maxCount: maxItemsOnQueue + 1);
var produceLimiter = new SemaphoreSlim(initialCount: maxItemsOnQueue, maxCount: maxItemsOnQueue);
// Kick off producers
var producerTasks = Enumerable.Range(0, threadCount)
.Select(index => Task.Run(() => ProduceAsync(queue, produceLimiter, consumerLimiter)));
// When production ends, send a termination signal to the consumer.
var endOfProductionTask = Task.WhenAll(producerTasks).ContinueWith(_ => consumerLimiter.Release());
// The consumer loop.
while (true)
{
// Wait for an item to be produced, or a signal for the end of production.
await consumerLimiter.WaitAsync();
// Get a produced item.
if (queue.TryDequeue(out var item))
{
// Tell producers that they can keep producing.
produceLimiter.Release();
// Yield a produced item.
await yield.ReturnAsync(item);
}
else
{
// If the queue is empty, the production is over.
break;
}
}
});
e.ForEachAsync((item, index) => Console.WriteLine($"{index + 1}: {item}")).Wait();
}
static async Task ProduceAsync(ConcurrentQueue<int> queue, SemaphoreSlim produceLimiter, SemaphoreSlim consumerLimiter)
{
var rnd = new Random();
for (var i = 0; i < 10; i++)
{
await Task.Delay(10);
var value = rnd.Next();
await produceLimiter.WaitAsync(); // Wait for the next production slot
queue.Enqueue(value); // Produce item on the queue
consumerLimiter.Release(); // Notify the consumer
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.