簡體   English   中英

C#AsyncEnumerable運行/等待多個任務從未完成

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

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