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

  • 共享内存
  • Actor并发编程模型
  • 202309Demo反省

吃点好的,很有必要。今天介绍常见的两种并发模型: 共享内存&Actor

共享内存编程模型

面向对象编程中,万物都是对象,数据+行为=对象;

多核时代,可并行多个线程,但是受限于资源对象,线程之间存在对共享内存的抢占/等待,实质是多线程调用对象的行为方法,这涉及#线程安全#线程同步#。

假如现在有一个任务,找100000以内的素数的个数,如果用共享内存的方法,代码如下:

可以看到,这些线程共享了sum变量,对sumsum++操作时必须上锁。

ini 复制代码
using System;
using System.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;

/// <summary>
/// 利用并行编程库Parallel,计算10000内素数的个数
/// </summary>
namespace Paralleler
{
    class Program
    {
        static object syncObj = new object();
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            ShareMemory();
            sw.Stop();
            Console.WriteLine($"共享内存并发模型耗时:{sw.Elapsed}");
        }

        static void ShareMemory()
        {
            var sum = 0;
            Parallel.For(1, 100000 + 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)
                {
                    lock(syncObj)
                    {
                        sum++;   // 共享了sum对象,"++"就是调用sum对象的成员方法
                    }
                }
            });
            Console.WriteLine($"1-10000内质数的个数是{sum}");
        }
    }
}

共享内存更贴合"面向对象开发者的固定思维", 强调线程对于资源的掌控力。

Actor并发模型

Actor模型则认为一切皆是Actor,Actor模型内部的状态由自己的行为维护,外部线程不能直接调对象的行为,必须通过消息才能激发行为,也就是消息传递机制来代替共享内存模型对成员方法的调用, 这样保证Actor内部数据只能被自己修改。

还是找到10000内的素数,我们使用.NET TPL Dataflow来完成,代码如下:

每个Actor的产出物就是流转到下一个Actor的消息。

ini 复制代码
using System;
using System.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks.Dataflow;
using System.Diagnostics;

/// <summary>
/// 利用并行编程库Paralleler,计算10000内素数的个数
/// </summary>
namespace Paralleler
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            Actor();
            sw.Stop();
            Console.WriteLine($"Actor并发模型耗时:{sw.Elapsed}");  
        }

        static void Actor()
        {
            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 { MaxDegreeOfParallelism=Environment.ProcessorCount});  // 默认MaxDegreeOfParallelism =1, 这里可充分利用多核能力
           
            var sum = 0;
            var actionBlock = new ActionBlock<bool>(x=>
            {
                if (x == true)
                    sum++;
            },new ExecutionDataflowBlockOptions {  });      // 涉及外部共享变量sum, 不能加锁的话,就挨个处理,使用默认并发度1
            transfromBlock.LinkTo(actionBlock, linkOptions);
            for (int i = 1; i <= 100000; i++)
            {
                transfromBlock.Post(i);
            }
            transfromBlock.Complete();       // 通知头部,不再投递了; 会将信息传递到下游。
            actionBlock.Completion.Wait();  // 等待尾部执行完成
            Console.WriteLine($"1-10000内质数的个数是{sum}");
        }
    }
}

TPL datflow中的所有块默认是单线程的 (并发度MaxDegreeOfParallelism= 1);意味着TransformBlock 和ActionBlock默认都是一个线程挨个处理消息。

当数据流块需要执行长时间运行的计算并且可从并行处理消息中获益时, 可以开启多核能力。

202309Demo反省

[求质数数量的例子]来对比 共享内存/actor其实并不合适。

第二阶段必然涉及最终共享变量的求和,违背了Actor内封状态和行为、由消息驱动的本质特征。


Actor模型最常见的使用场景是: 生产者消费者模型。

csharp 复制代码
  static  void Actor1()
        {
            var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
            var transfromBlock = new TransformBlock<int,string>(x=> 
            { 
               Thread.Sleep(1000); // 模拟生产时间
               Console.WriteLine($"生产者0生产了 {0}:{x}");  
               return $"0:{x}";
            }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = Environment.ProcessorCount });   
           
            var actionBlock = new ActionBlock<string>(x=>
            {
                    Console.WriteLine("消费者消费了 {0}", x);  
                    Thread.Sleep(1000); // 模拟消费时间  
            },new ExecutionDataflowBlockOptions {  MaxDegreeOfParallelism = Environment.ProcessorCount });      // 这里无所谓, 并没有线程安全问题。
            transfromBlock.LinkTo(actionBlock, linkOptions);

            // 准备从pipeline头部开始投递
            for (int i = 0; i < 10; i++)
            {
                transfromBlock.Post(i);
            }
            transfromBlock.Complete();  // 通知头部,不再投递了; 会将信息传递到下游。
            actionBlock.Completion.Wait();  // 等待尾部执行完成
        }

总结

  1. 共享内存并发模型,需要开发者重视多线程对于共享代码的掌控力。
  2. Actor模型强调内封状态和行为、由消息驱动,由运行时维护actor之间的依赖(编码阶段提前定义了依赖关系),降低了编写高性能,低延迟应用的难度(开发者不再需要自行维护代码线程安全)。
  3. Golang使用的Channel类Actor模型,使用Channel进一步解耦了调用的参与方,你都不用关注下游提供者是谁。
相关推荐
编码浪子33 分钟前
构建一个rust生产应用读书笔记7-确认邮件2
开发语言·后端·rust
昙鱼44 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
白宇横流学长1 小时前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
kirito学长-Java1 小时前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
程序猿-瑞瑞1 小时前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
组合缺一2 小时前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·后端·spring·solon
猿来入此小猿2 小时前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码2 小时前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
栗豆包2 小时前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
万亿少女的梦1682 小时前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端