簡體   English   中英

Parallel.ForEach 中的多個異步等待鏈接

[英]Multiple async-await chaining inside Parallel.ForEach

我有一個 Parallel.ForEach 循環,它遍歷一個集合。 在循環內部,我進行了多次網絡 I/O 調用。 我使用了 Task.ContinueWith 並嵌套了隨后的 async-await 調用。 處理的順序無關緊要,但是每個異步調用的數據應該以同步的方式處理。 含義 - 對於每次迭代,從第一個異步調用檢索到的數據應該傳遞給第二個異步調用。 在第二個異步調用完成后,來自兩個異步調用的數據應該一起處理。

Parallel.ForEach(someCollection, parallelOptions, async (item, state) =>
{
    Task<Country> countryTask = Task.Run(() => GetCountry(item.ID));

    //this is my first async call
    await countryTask.ContinueWith((countryData) =>
    {
        countries.Add(countryData.Result);

        Task<State> stateTask = Task.Run(() => GetState(countryData.Result.CountryID));

        //based on the data I receive in 'stateTask', I make another async call
        stateTask.ContinueWith((stateData) =>
        {
            states.Add(stateData.Result);

            // use data from both the async calls pass it to below function for some calculation
            // in a synchronized way (for a country, its corresponding state should be passed)

            myCollection.ConcurrentAddRange(SomeCalculation(countryData.Result, stateData.Result));
        });
    });
});

我在沒有使用 continue await 的情況下嘗試了上述方法,但它沒有以同步方式工作。 現在,上面的代碼執行完成,但沒有記錄被處理。

請問有什么幫助嗎? 讓我知道是否可以添加更多詳細信息。

由於您的方法涉及 I/O,因此它們應該被編寫為真正異步的,而不僅僅是使用Task.Run在線程池上同步運行。

然后您可以將Task.WhenAllEnumerable.Select結合使用:

var tasks = someCollection.Select(async item =>
{
    var country = await GetCountryAsync(item.Id);
    var state = await GetStateAsync(country.CountryID);
    var calculation = SomeCalculation(country, state);

    return (country, state, calculation);
});

foreach (var tuple in await Task.WhenAll(tasks))
{
    countries.Add(tuple.country);
    states.Add(tuple.state);
    myCollection.AddRange(tuple.calculation);
}

這將確保每個country /地區 > state > calculation順序發生,但每個item都是同時異步處理的。


根據評論更新

using var semaphore = new SemaphoreSlim(2);
using var cts = new CancellationTokenSource();

int failures = 0;

var tasks = someCollection.Select(async item =>
{
    await semaphore.WaitAsync(cts.Token);
    
    try
    {
        var country = await GetCountryAsync(item.Id);
        var state = await GetStateAsync(country.CountryID);
        var calculation = SomeCalculation(country, state);

        Interlocked.Exchange(ref failures, 0);

        return (country, state, calculation);
    {
    catch
    {
        if (Interlocked.Increment(ref failures) >= 10)
        {
            cts.Cancel();
        }
        throw;
    }
    finally
    {
        semaphore.Release();
    }
});

信號量保證最多 2 個並發異步操作,取消令牌將在連續 10 次異常后取消所有未完成的任務。

Interlocked方法確保以線程安全的方式訪問failures


進一步更新

使用 2 個信號量來防止多次迭代可能更有效。

將所有列表添加封裝到一個方法中:

void AddToLists(Country country, State state, Calculation calculation)
{
    countries.Add(country);
    states.Add(state);
    myCollection.AddRange(calculation);
}

然后,您可以允許 2 個線程同時服務 Http 請求,並允許 1 個線程執行添加,使該操作線程安全:

using var httpSemaphore = new SemaphoreSlim(2);
using var listAddSemaphore = new SemaphoreSlim(1);
using var cts = new CancellationTokenSource();

int failures = 0;

await Task.WhenAll(someCollection.Select(async item =>
{
    await httpSemaphore.WaitAsync(cts.Token);
    
    try
    {
        var country = await GetCountryAsync(item.Id);
        var state = await GetStateAsync(country.CountryID);
        var calculation = SomeCalculation(country, state);

        await listAddSemaphore.WaitAsync(cts.Token);
        AddToLists(country, state, calculation);

        Interlocked.Exchange(ref failures, 0);
    {
    catch
    {
        if (Interlocked.Increment(ref failures) >= 10)
        {
            cts.Cancel();
        }
        throw;
    }
    finally
    {
        httpSemaphore.Release();
        listAddSemaphore.Release();
    }
}));

我認為你過於復雜了; Parallel.ForEach內部,您已經在線程池中,因此在內部創建大量額外任務確實沒有任何好處。 所以; 如何做到這一點實際上取決於GetState等是同步的還是異步的。 如果我們假設同步,那么類似:

Parallel.ForEach(someCollection, parallelOptions, (item, _) =>
{
    var country = GetCountry(item.Id);

    countries.Add(country); // warning: may need to synchronize

    var state = GetState(country.CountryID);

    states.Add(state); // warning: may need to synchronize

    // use data from both the async calls pass it to below function for some calculation
    // in a synchronized way (for a country, its corresponding state should be passed)
    myCollection.ConcurrentAddRange(SomeCalculation(country, state));
});

如果它們是異步的,那就更尷尬了; 如果我們能做這樣的事情會很好

// WARNING: DANGEROUS CODE - DO NOT COPY
Parallel.ForEach(someCollection, parallelOptions, async (item, _) =>
{
    var country = await GetCountryAsync(item.Id);

    countries.Add(country); // warning: may need to synchronize

    var state = await GetStateAsync(country.CountryID);

    states.Add(state); // warning: may need to synchronize

    // use data from both the async calls pass it to below function for some calculation
    // in a synchronized way (for a country, its corresponding state should be passed)
    myCollection.ConcurrentAddRange(SomeCalculation(country, state));
});

但這里的問題是Parallel.ForEach中的回調都不是“可等待的”,這意味着:我們在這里默默地創建了一個async void回調,即:非常糟糕。 這意味着一旦未完成的await發生, Parallel.ForEach就會認為它已經“完成”,這意味着:

  1. 我們不知道所有工作何時真正完成
  2. 您可能會比您預期的同時做更多的事情(不能尊重 max-dop)

目前似乎沒有任何好的 API 可以避免這種情況。

暫無
暫無

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

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