共享內(nèi)存 & Actor并發(fā)模型到底哪個(gè)快?
本文轉(zhuǎn)載自微信公眾號(hào)「精益碼農(nóng)」,作者有態(tài)度的馬甲。轉(zhuǎn)載本文請(qǐng)聯(lián)系精益碼農(nóng)公眾號(hào)。
先說(shuō)結(jié)論
1.首先兩者對(duì)于并發(fā)的風(fēng)格模型不一樣。
共享內(nèi)存利用多核CPU的優(yōu)勢(shì),使用強(qiáng)一致的鎖機(jī)制控制并發(fā), 各種鎖交織,稍不注意可能出現(xiàn)死鎖,更適合熟手。
Actor模型易于控制和管理,以消息觸發(fā)、流水線挨個(gè)處理,天然分布式,思路清晰。
2.真要說(shuō)性能,求100_000 以內(nèi)的素?cái)?shù)的個(gè)數(shù)]場(chǎng)景 & 電腦8c 16g的配置
- 2.1 理論上如果以默認(rèn)的Actor并發(fā)模型來(lái)做這個(gè)事情,共享內(nèi)存模型是優(yōu)于Actor模型的;
- 2.2 上文中我對(duì)于Actor做了多線程優(yōu)化,Actor模型性能慢慢追上來(lái)了。
下面請(qǐng)聽(tīng)我嘮嗑。
默認(rèn)Actor模型
計(jì)算[100_000內(nèi)素?cái)?shù)的個(gè)數(shù)], 分為兩步:
(1) 迭代判斷當(dāng)前數(shù)字是不是素?cái)?shù)
(2) 如果是素?cái)?shù),執(zhí)行sum++
完成以上兩步,共享內(nèi)存模型均能充分利用CPU多核心。
Actor模型:與TPL中的原語(yǔ)不同,TPL Datflow中的所有塊默認(rèn)是單線程的,這就意味著完成以上兩步的TransfromBlock和ActionBlock都是以一個(gè)線程挨個(gè)處理消息數(shù)據(jù) (這也是Dataflow的設(shè)計(jì)初衷,形成清晰單純的流水線)。
猜測(cè)此時(shí):共享內(nèi)存相比默認(rèn)的Actor模型更具優(yōu)勢(shì)。
使用NUnit做單元測(cè)試,數(shù)據(jù)量從小到大: 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]任一數(shù)字整除,就不是質(zhì)數(shù)
- f = false;
- }
- if (f == true)
- {
- Interlocked.Increment(ref sum);// 共享了sum對(duì)象,“++”就是調(diào)用sum對(duì)象的成員方法
- }
- });
- Console.WriteLine($"1-{num}內(nèi)質(zhì)數(shù)的個(gè)數(shù)是{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]任一數(shù)字整除,就不是質(zhì)數(shù)
- 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);
- // 準(zhǔn)備從pipeline頭部開(kāi)始投遞
- 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(); // 通知頭部,不再投遞了; 會(huì)將信息傳遞到下游。
- actionBlock.Completion.Wait(); // 等待尾部執(zhí)行完
- Console.WriteLine($"1-{num} Prime number include {sum}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"1-{num} cause exception.",ex);
- }
- }
- }
- }
測(cè)試結(jié)果如下:
測(cè)試結(jié)果印證我說(shuō)的結(jié)論2.1
優(yōu)化后的Actor模型
那后面我對(duì)Actor做了什么優(yōu)化呢? 能產(chǎn)生下圖的2.2結(jié)論。
請(qǐng)重新回看《三分鐘掌握共享內(nèi)存 & Actor并發(fā)模型》 TransfromBlock 塊的細(xì)節(jié):
- 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]任一數(shù)字整除,就不是質(zhì)數(shù)
- f = false;
- }
- return f;
- }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism=50, EnsureOrdered = false }); // 這里開(kāi)啟多線程并發(fā)
上面說(shuō)到默認(rèn)的Actor是以單線程處理輸入的消息,此次我們對(duì)這個(gè)TransfromBlock 塊設(shè)置了MaxDegreeOfParallelism 參數(shù),
這個(gè)參數(shù)能在Actor中開(kāi)啟多線程并發(fā)執(zhí)行,但是這里面就不能有共享變量(否則你又得加鎖),恰好我們完成 (1) 迭代判斷當(dāng)前數(shù)字是不是素?cái)?shù)這一步并不依賴共享對(duì)象,所以這(1)步開(kāi)啟多線程以后性能與共享內(nèi)存模型基本沒(méi)差別。
那為什么總體性能慢慢超過(guò)共享內(nèi)存?
這是因?yàn)閳?zhí)行第二步(2) 如果是素?cái)?shù),執(zhí)行sum++, 共享內(nèi)存要加/解鎖,線程切換; 而Actor單線程挨個(gè)處理, 總體上Actor就略勝共享內(nèi)存模型了。
這里再次強(qiáng)調(diào),Actor模型執(zhí)行第二步(2) 如果是素?cái)?shù),執(zhí)行sum++,不可開(kāi)啟MaxDegreeOfParallelism,因?yàn)橐蕾嚵斯蚕碜兞縮um
結(jié)束語(yǔ)
That's All, 感謝.NET圈紀(jì)檢委@懶得勤快促使我重溫了單元測(cè)試的寫(xiě)法 & 深度分析Actor模型風(fēng)格。
請(qǐng)大家仔細(xì)對(duì)比結(jié)論和上圖,脫離場(chǎng)景和硬件環(huán)境談性能就是耍流氓,理解不同并發(fā)模型的風(fēng)格和能力是關(guān)鍵, 針對(duì)場(chǎng)景和未來(lái)的拓展性、可維護(hù)性、可操作性做技術(shù)選型 。