HI,前幾天被.NET圈紀檢委@懶得勤快問到
共享記憶體
和Actor
併發模型哪個速度更快。
前文傳送門:
說實在,我內心10w頭羊駝跑過......
先說結論
- 首先兩者對於併發的風格模型不一樣。
共享記憶體利用多核CPU的優勢,使用強一致的鎖機制控制併發, 各種鎖交織,稍不注意可能出現死鎖,更適合熟手。
Actor模型易於控制和管理,以訊息觸發,流水線挨個處理, 思路清晰。
- 真要說效能,
求100000 以內的素數的個數]場景
&我電腦8c 16g的配置
, 我根據這個示例拍腦袋對比。。。。。
2.1 理論上如果以預設的Actor併發模型來做這個事情,Actor的效能是遜於共享記憶體模型的;
2.2 上文中我對於Actor做了多執行緒優化,效能慢慢追上來了。
預設Actor模型
計算[100_000內素數的個數], 分為兩步:
(1) 迭代判斷當前數字是不是素數
(2) 如果是素數,執行sum++
共享記憶體完成以上兩步, 均能充分利用CPU多核心。
Actor模型:與TPL中的原語不同,TPL datflow中的所有塊預設是單執行緒的,這就意味著完成以上兩步的TransfromBlock
和ActionBlock
都是以一個執行緒挨個處理訊息資料(這也是Dataflow的設計初衷,形成清晰單純的流水線)。
猜測起來也是共享記憶體相比預設的Actor模型更具優勢。
使用NUnit做單元測試,資料量從小到大: 10_000,50_000,100_000,200_000,300_000,500_000
using NUnit.Framework;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks.Dataflow;
namespace TestProject2
{
public class Tests
{
[TestCase(10_000)]
[TestCase(50_000)]
[TestCase(100_000)]
[TestCase(200_000)]
[TestCase(300_000)]
[TestCase(500_000)]
public void ShareMemory(int num)
{
var sum = 0;
Parallel.For(1, num + 1, (x, state) =>
{
var f = true;
if (x == 1)
f = false;
for (int i = 2; i <= x / 2; i++)
{
if (x % i == 0) // 被[2,x/2]任一數字整除,就不是質數
f = false;
}
if (f == true)
{
Interlocked.Increment(ref sum);// 共享了sum物件,“++”就是呼叫sum物件的成員方法
}
});
Console.WriteLine($"1-{num}內質數的個數是{sum}");
}
[TestCase(10_000)]
[TestCase(50_000)]
[TestCase(100_000)]
[TestCase(200_000)]
[TestCase(300_000)]
[TestCase(500_000)]
public async Task Actor(int num)
{
var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
var bufferBlock = new BufferBlock<int>();
var transfromBlock = new TransformBlock<int, bool>(x =>
{
var f = true;
if (x == 1)
f = false;
for (int i = 2; i <= x / 2; i++)
{
if (x % i == 0) // 被[2,x/2]任一數字整除,就不是質數
f = false;
}
return f;
}, new ExecutionDataflowBlockOptions { EnsureOrdered = false });
var sum = 0;
var actionBlock = new ActionBlock<bool>(x =>
{
if (x == true)
sum++;
}, new ExecutionDataflowBlockOptions { EnsureOrdered = false });
transfromBlock.LinkTo(actionBlock, linkOptions);
// 準備從pipeline頭部開始投遞
try
{
var list = new List<int> { };
for (int i = 1; i <= num; i++)
{
var b = await transfromBlock.SendAsync(i);
if (b == false)
{
list.Add(i);
}
}
if (list.Count > 0)
{
Console.WriteLine($"md,num post failure,num:{list.Count},post again");
// 再投一次
foreach (var item in list)
{
transfromBlock.Post(item);
}
}
transfromBlock.Complete(); // 通知頭部,不再投遞了; 會將資訊傳遞到下游。
actionBlock.Completion.Wait(); // 等待尾部執行完
Console.WriteLine($"1-{num} Prime number include {sum}");
}
catch (Exception ex)
{
Console.WriteLine($"1-{num} cause exception.",ex);
}
}
}
}
測試結果如下:
測試結果印證我說的結論2.1
優化後的Actor模型
那後面我對Actor做了什麼優化呢?能產生下圖的結論。
請重新回看《三分鐘掌握》 TransformBlock塊的細節:
var transfromBlock = new TransformBlock<int, bool>(x =>
{
var f = true;
if (x == 1)
f = false;
for (int i = 2; i <= x / 2; i++)
{
if (x % i == 0) // 被[2,x/2]任一數字整除,就不是質數
f = false;
}
return f;
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism=50, EnsureOrdered = false });
上面說到預設的Actor是單執行緒處理輸入的訊息, 此時我們設定了MaxDegreeOfParallelism
引數,引數能在Actor中開啟多執行緒併發執行,但是這裡面就不能有共享變數(否則你又得加鎖),恰好我們完成 (1) 迭代判斷當前數字是不是素數
這一步並不依賴共享物件,所以這一步效能與共享記憶體模型基本沒差別。
那為什麼總體效能慢慢超過共享記憶體?
這是因為執行第二步(2) 如果是素數,執行sum++
, 共享記憶體要加解鎖,執行緒上下文切換,而Actor單執行緒挨個處理, 總體就略勝共享記憶體模型了。
這裡再次強調,Actor模型執行第二步
(2) 如果是素數,執行sum++
,不可開啟MaxDegreeOfParallelism
,因為依賴了共享變數sum
結束語
請大家仔細對比結論和上圖,脫離場景和硬體環境談效能就是耍流氓,理解不同併發模型的風格和能力是關鍵,本文僅針對這個示例拍腦袋對比。
實際要針對場景和未來的拓展性、可維護性、可操作性做技術選型 。
That's All, 感謝.NET圈紀檢委@懶得勤快促使我重溫了單元測試的寫法 & 深度分析Actor模型。