共享內存 & Actor併發模型哪個更快? 先說結論 默認Actor模型 優化後的Actor模型 那爲什麼總體性能慢慢超過共享內存? 結束語

HI,前幾天被.NET圈紀檢委@懶得勤快問到共享內存和Actor併發模型哪個速度更快。

前文傳送門:

說實在,我內心10w頭羊駝跑過......

先說結論

  1. 首先兩者對於併發的風格模型不一樣。

共享內存利用多核CPU的優勢,使用強一致的鎖機制控制併發, 各種鎖交織,稍不注意可能出現死鎖,更適合熟手。

Actor模型易於控制和管理,以消息觸發,流水線挨個處理, 思路清晰。

  1. 真要說性能,求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模型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章