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模型。