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

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

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

共享内存编程模型

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

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

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

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

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进一步解耦了调用的参与方,你都不用关注下游提供者是谁。
相关推荐
金銀銅鐵3 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路4 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x4 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn6 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈7 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780517 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123457 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒7 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记7 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598788 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端