深入了解C#(TPL)之Parallel.ForEach異步

前言

最近在做項目過程中使用到了如題并行方法,當時還是有點猶豫不決,因為平常使用不多, 於是藉助周末時間稍微深入了下,發現我用錯了,故此做一詳細記錄,希望對也不是很了解的童鞋在看到此文後不要再犯和我同樣的錯誤。

并行遍歷異步表象

這裏我們就不再講解該語法的作用以及和正常遍歷處理的區別,網上文章比比皆是,我們直接進入主題,本文所演示程序在控制台中進行。可能大部分童鞋都是如下大概這樣用的

Parallel.ForEach(Enumerable.Range(0, 10), index =>
{
    Console.WriteLine(index);
});

 

我們採取并行方式遍歷10個元素,然後結果也隨機打印出10個元素,一點毛病也沒有。然而我是用的異步方式,如下:

Parallel.ForEach(Enumerable.Range(0, 10), async index =>
{
    await AsyncTask(index);
});
static async Task<int> AsyncTask(int i)
{
    await Task.Delay(100);
    
    var calculate = i * 2;
    
    Console.WriteLine(calculate);

    return calculate;
}

我們只是將并行操作更改為了異步形式,然後對每個元素進行對應處理,打印無序結果,一切也是如我們所期望,接下來我再來看一個例子,經過并行異步處理后猜猜最終字典中元素個數可能或一定為多少呢?

var dicts = new ConcurrentDictionary<string, int>();

Parallel.ForEach(Enumerable.Range(0, 10), async index =>
{
    var result = await AsyncTask(index);

    dicts.TryAdd(index.ToString(), result);
});

Console.WriteLine($"element count in dictionary {dicts.Count}");

 

如果對該并行方法沒有深入了解的話,大概率都會猜錯,我們看到字典中元素為0,主要原因是用了異步后引起的,為何會這樣呢?我們首先從表象上來分析,當我們在控制台上對并行方法用了異步后,你會發現編譯器會告警(主函數入口已用異步標識),如下:

接下來我們再來看看調用該并行異步方法的最終調用構造,如下:

public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource> body);

第二個參數為內置委託Action,所以我們也可以看出並不能用於異步,因為要是異步至少也是Func<Task>,比如如下方法參數形式

static async Task AsyncDemo(Func<int,Task> func)
{
    await func(1);
}

并行遍歷異步本質

通過如上表象的分析我們得出并行遍歷方法應該是並不支持異步(通過最終結果分析得知,表述為不能用於異步更恰當),但是在實際項目開發中我們若沒有注意到該方法的構造很容易就會誤以為支持異步,如我一樣寫完也沒報錯,也就草草了事。那麼接下來我們反編譯看下最終實際情況會是怎樣的呢。

進入主函數,我們已將主函數進行異步標識,所以將主函數放在狀態機中執行(狀態機類,<Main>d_0),這點我們毫無保留的贊同,接下來實例化字典,並通過并行遍歷異步處理元素集合併將其結果嘗試放入到字典中

由上我們可以看到主函數是在狀態機中運行且構造為AsyncTaskMethodBuilder,當我們通過并行遍歷異步處理時每次都會實例化一個狀態機類即如上<<Main>b__0>d,但我們發現此狀態機的構造是AsyncVoidMethodBuilder,利用此狀態機類來異步處理每一個元素,如下

最終調用AsyncTask異步方法,這裏我就不再截圖,同樣也是生成一個此異步方法的狀態機類。稍加分析想必我們已經知曉結果,AsyncTaskMethodBuilder指的就是(async task),而AsyncVoidMethodBuilder指的是(async void),所以對并行遍歷異步操作是將其隱式轉換為async void,而不是async task,這也和我們從其構造為Action得出的結論一致,我們知道(async void)僅限於基於事件的處理程序(常見於客戶端應用程序),其他情況避免用async void,也就是說將返回值放在Task或Task<T>中。當并行執行任務時,由於返回值為void,不會等待操作完成,這也就不難解釋為何字典中元素個數為0。

總結

當時並沒有過多的去了解,只是想當然的認為用了異步也沒出現編譯報錯,但是又由於沒怎麼用過,我還是抱着懷疑的態度,於是再深究了下,發現用法是大錯特錯。通過構造僅接受為Action委託,這也就意味着根本無法等待異步操作完成,之所以能接受異步索引其本質是隱式轉換為(async void),從另外一個角度看,異步主要用於IO密集型,而并行處理用於CPU密集型計算,基於此上種種一定不能用於異步,否則結果你懂的。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

您可能也會喜歡…