三分钟掌握共享内存 & 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(x=> { Thread.Sleep(1000); // 模拟生产时间 Console.WriteLine($"生产者0生产了 {0}:{x}"); return $"0:{x}"; }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }); var actionBlock = new ActionBlock(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进一步解耦了调用的参与方,你都不用关注下游提供者是谁。

相关推荐
Minyy112 小时前
SpringBoot程序的创建以及特点,配置文件,LogBack记录日志,配置过滤器、拦截器、全局异常
xml·java·spring boot·后端·spring·mybatis·logback
画个大饼4 小时前
Go语言实战:快速搭建完整的用户认证系统
开发语言·后端·golang
iuyou️9 小时前
Spring Boot知识点详解
java·spring boot·后端
一弓虽9 小时前
SpringBoot 学习
java·spring boot·后端·学习
姑苏洛言9 小时前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
光而不耀@lgy10 小时前
C++初登门槛
linux·开发语言·网络·c++·后端
方圆想当图灵10 小时前
由 Mybatis 源码畅谈软件设计(七):SQL “染色” 拦截器实战
后端·mybatis·代码规范
毅航10 小时前
MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑
java·后端·mybatis
我的golang之路果然有问题11 小时前
速成GO访问sql,个人笔记
经验分享·笔记·后端·sql·golang·go·database
柏油11 小时前
MySql InnoDB 事务实现之 undo log 日志
数据库·后端·mysql