共享内存 & Actor并发模型到底哪个快?

共 13152字,需浏览 27分钟

 ·

2021-08-02 19:01



HI,前几天被.NET圈纪检委@懒得勤快问到共享内存Actor并发模型哪个速度更快。

前文传送门:《三分钟掌握共享内存 & Actor并发模型

说实在,我内心10w头羊驼跑过.....

先说结论

1.首先两者对于并发的风格模型不一样。

共享内存利用多核CPU的优势,使用强一致的锁机制控制并发, 各种锁交织,稍不注意可能出现死锁,更适合熟手。

Actor模型易于控制和管理,以消息触发、流水线挨个处理,天然分布式,思路清晰。

2.真要说性能,求100_000 以内的素数的个数]场景 & 电脑8c 16g的配置

2.1 理论上如果以默认的Actor并发模型来做这个事情,共享内存模型是优于Actor模型的;2.2 上文中我对于Actor做了多线程优化,Actor模型性能慢慢追上来了。
下面请听我唠嗑。

默认Actor模型

计算[100_000内素数的个数], 分为两步:
(1) 迭代判断当前数字是不是素数
(2) 如果是素数,执行sum++

完成以上两步,共享内存模型均能充分利用CPU多核心。

Actor模型:与TPL中的原语不同,TPL Datflow中的所有块默认是单线程的,这就意味着完成以上两步的TransfromBlockActionBlock都是以一个线程挨个处理消息数据 (这也是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做了什么优化呢?  能产生下图的2.2结论。

请重新回看《三分钟掌握共享内存 & Actor并发模型TransfromBlock 块的细节:

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是以单线程处理输入的消息,此次我们对这个TransfromBlock 块设置MaxDegreeOfParallelism 参数,

这个参数能在Actor中开启多线程并发执行,但是这里面就不能有共享变量(否则你又得加锁),恰好我们完成 (1) 迭代判断当前数字是不是素数这一步并不依赖共享对象,所以这(1)步开启多线程以后性能与共享内存模型基本没差别。

那为什么总体性能慢慢超过共享内存?

这是因为执行第二步(2) 如果是素数,执行sum++, 共享内存要加/解锁,线程切换; 而Actor单线程挨个处理, 总体上Actor就略胜共享内存模型了。

这里再次强调,Actor模型执行第二步(2) 如果是素数,执行sum++,不可开启MaxDegreeOfParallelism,因为依赖了共享变量sum

结束语

That's All, 感谢.NET圈纪检委@懒得勤快促使我重温了单元测试的写法 & 深度分析Actor模型风格。

请大家仔细对比结论和上图脱离场景和硬件环境谈性能就是耍流氓,理解不同并发模型的风格和能力是关键, 针对场景和未来的拓展性、可维护性、可操作性做技术选型 。

本文内容和制图均为原创,文章永久更新地址请参阅左下角原文,如对您有所帮助,请一键三连,方便的话置一个星标 ~。。~。

专题相关 一网打尽

  三分钟掌握共享内存 & Actor并发模型

 你管这叫"线程安全"?

 .Net线程同步技术解读

 Redis分布式锁抽丝剥茧

 看过这么多爆文,依旧走不好异步编程这条路?

 TPL Dataflow组件应对高并发,低延迟要求

 如何利用.NETCore向Azure EventHubs准实时批量发送数据?

 难缠的布隆过滤器,这次终于通透了

扫码关注我们

不会让您失望的。

浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报