Orleans 分布式单线程高并发

程序在运行过程中有时会莫名其妙出现代码的某些约束或者执行结果和理想状况不一样,正常逻辑怎么会出现这样的情况?到底发生了什么?好像见了鬼!瞬间好无助。

 

谁来救救我

大多数出现正常逻辑很难解释的时候,我们可能会想到并发问题,因为好像只有并发才会能说服自己。为了验证和解决这个问题,我们可能会尝试一些方案,在并发的情况下我相信很多人都使用过锁,锁确实也能帮忙我们解决问题,不然它干嘛存在。

但随着业务逻辑的持续复杂,锁的使用可能无处不在。首先大家都知道锁本身的机制很耗性能;然后锁本身不涉及什么编程模式,所以在业务代码中融入大量锁对代码本身的稳定性也有一定影响。

经过查找资料,因为本身的项目是基于 .NET,所以发现 Microsoft Orleans 好像可以比较好的满足解决并发的需求。

Orleans 之前,先来扯一扯 Actor 模型

  1. Actor 是以单线程存在的,所有消息都是顺序到达的,每次收到消息后,就放入队列,而它每次也从队列中取出消息体来处理;

  2. 每一个 Actor 有一个 Id 和它对应,一个 Id 对应的 Actor 只会在 集群中 存在一个,使用者只需要通过 Id 就能随时访问不需要关注该 Actor 在集群的什么位置;

  3. 每一个 Actor 看作是一个独立的实体,拥有自己独立的状态。Actor 与 Actor 之间可以进行消息通知;

注:有状态的 Actor 在集群中一个 Id 只会存在一个实例,无状态的可配置为根据流量存在多个,无状态的情况看具体业务需求。

 

Actor System

再来扯一扯 Orleans 框架

Orleans 提供了一个简单的方法来构建大规模、高并发、分布式应用程序,被认为是 Actor 模型的分布式版本,是一种改进的 Actor 模型。在 Orleans 中,Actors 被称作 Grains,采用接口来表示,Actors 的消息用异步方法来接收,方法返回值必须是 Task or Task<T>。

Orleans几个核心角色:

Grains(Actors)

Grains 是 Orleans 应用程序的业务逻辑实现与抽象,Grains 是彼此孤立的原子单位,分布的,持久的。 一个典型的 Grain 是有状态和行为的一个单实例。

Silo

Silo 是一个主机服务,里面主要用于执行 Grains,也就是说 Grains 开发完成后需要注册到 Silo 中,然后等待调用。它监听一个端口,用来监听从 Silo 到 Silo 的消息或者从客户端到 Silo 的消息的,典型的 Silo 就是,每台机器运行一个 Silo,会对外暴露网关地址供调用。

Cluster(集群搭建的时候会具体介绍)

大量的 Silo 同时在一起工作就形成了 Orleans 的集群,Orleans 运行完全自动化的集群管理。

Client

具体的应用客户端,可以是控制台、Web 应用程序、WPF 等一切 .NET 端技术。


开始接触 Orleans Sample 的时候,第一感觉项目结构和 gRPC 还挺像的,如果你之前有接触,一定感觉很亲切:

  1. 定义一个接口(Interfaces)
  2. 实现接口(Grains) -- 添加引用Interfaces
  3. 启动服务端(Silo)-- 添加引用Interfaces,Grains
  4. 启动客户端 (Client)-- 添加引用Interfaces

练习过程中对 NuGet 安装 Orleans 相关依赖包可能会有一些模糊,这里说明一下我的具体步骤,希望尽快帮忙实现效果,所有程序集使用 .NET Framework 的版本都是 4.6:

程序集名称类型NuGet 依赖包
Microsoft.Orleans.
引用
Interfaces类库Core-
Grains类库CoreInterfaces
Silo控制台程序Core
OrleansCodeGenerator
OrleansProviders
OrleansRuntime
Interfaces
Grains
Client控制台程序Core
OrleansCodeGenerator
Interfaces

在 Silo 项目中添加配置文件 OrleansConfiguration.xml:

 

<?xml version="1.0" encoding="utf-8" ?>
<OrleansConfiguration xmlns="urn:orleans">
  <Globals>
    <SeedNode Address="localhost" Port="11111" />
  </Globals>
  <Defaults>
    <Networking Address="localhost" Port="11111" />
    <ProxyingGateway Address="localhost" Port="30000" />
  </Defaults>
</OrleansConfiguration>

SeedNode:集群中主 Silo 地址,生产环境下不要这么使用。以这种方式配置主 Silo 的情况下,其他 Silo 加入集群需要等主 Silo 先启动。之后会介绍 SystemStore 来维护集群成员关系;
Networking:内部 Silo 与 Silo 之间通信地址;
ProxyingGateway:客户端调用的网关地址;

在 Client 项目中添加配置文件 ClientConfiguration.xml:

 

<?xml version="1.0" encoding="utf-8" ?>
<ClientConfiguration xmlns="urn:orleans">
  <Gateway Address="localhost" Port="30000"/>
</ClientConfiguration>

Gateway:配置 Silo 对外的网关地址;

集群下可以配置多个 Gateway 节点,如下:

 

<Gateway Address="gateway1" Port="30000"/>
<Gateway Address="gateway2" Port="30000"/>

注意:配置文件需要设置属性 "复制到输出目录"

 

configuration

Grain 说明:

每个 Grain 都是单实例的,具有唯一标识。根据唯一标识获取 Grain,这个标识可以是 GUID、String、Long、混合类型。

在 Grain 内如果发送消息给其他 Grain,需要使用 this.GrainFactory.GetGrain,不能通过 GrainClient.GrainFactory.GetGrain。

 

var test = GrainClient.GrainFactory.GetGrain<ITest>(0); // long类型的primaryKey 0

 

public class TestGrain : Orleans.Grain, ITest
{
  private int num = 0;

  public Task AddCount()
  {
    num++;
    Console.WriteLine(num);
    return Task.CompletedTask;
  }
}

Client 说明:

同时启动3个 Task,每个 Task 内并行200次调用 AddCount 方法。如果没有做特殊的处理,num 的结果肯定是乱的,并不会出现一直累加的效果。

 

private static void DoClientWork()
{
  var t1 = Task.Factory.StartNew(() =>
  {
    AddCount();
  });
  var t2 = Task.Factory.StartNew(() =>
  {
    AddCount();
  });
  var t3 = Task.Factory.StartNew(() =>
  {
    AddCount();
  });
  Task.WaitAll(t1, t2, t3);
}

static void AddCount()
{
  var test = GrainClient.GrainFactory.GetGrain<ITest>(0);

  Parallel.For(0, 200, (i) =>
  {
    test.AddCount();
  });
}

实际上执行最终的结果是600,并不会出现不一致的变化效果,这足以说明同一个 Grain 内部是单线程执行。

 

Test Result

参考链接:



作者:BeckJin
链接:https://www.jianshu.com/p/141ea382d242
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

<think>好的,我现在需要回答用户关于Actor模型如何实现线程安全的问题。首先,我得回忆一下Actor模型的基本概念,然后分析它的线程安全机制。 首先,Actor模型是一种并发计算模型,每个Actor是一个独立的实体,通过消息传递进行通信。用户想知道为什么Actor模型能实现线程安全,可能需要了解其设计原理。 我记得在传统多线程编程中,线程安全问题主要来自共享状态和竞争条件。而Actor模型通过封装状态到各个Actor内部,避免了共享内存,从而消除了竞争条件。每个Actor处理消息时是串行的,一次只处理一个消息,这样就不会有多个线程同时访问内部状态的情况。 接下来,需要详细说明消息传递机制。Actor之间不直接调用方法,而是发送不可变的消息。消息队列是每个Actor的邮箱,按照顺序处理,这样保证了顺序性。每个Actor内部的状态修改都是隔离的,不会被其他Actor干扰。 还需要提到监督机制和错误处理,这可能影响线程安全。比如,当Actor崩溃时,监督策略会处理,但具体如何保持状态安全可能需要解释。此外,位置透明性和分布式扩展也可能与线程安全有关,但用户可能更关注单机环境下的原理。 可能存在的误区是认为消息传递自动保证安全,但实际上消息必须不可变,否则如果发送可变对象,其他Actor修改会导致问题。这点需要强调,消息的不可变性是关键。 另外,对比传统锁机制,Actor模型通过设计避免了锁的使用,减少了死锁和竞态条件的风险。每个Actor内部的状态访问不需要锁,因为单线程处理。 总结时,需要分点列出Actor模型实现线程安全的关键点:封装状态、消息队列串行处理、消息不可变、无共享内存、基于事件的并发模型等。 可能还需要检查是否有遗漏的部分,比如Actor的邮箱如何确保消息顺序,是否使用特定的数据结构,或者是否有其他同步机制。但根据Actor模型的标准定义,通常不涉及复杂同步,主要依赖消息队列和单线程处理。 最后,确保用中文清晰、结构分明地解释这些点,避免使用专业术语过多,必要时举例说明,比如两个Actor同时发送消息如何处理,确保用户理解Actor的隔离和串行处理机制。</think>Actor模型实现线程安全的核心原理是通过**消息传递代替共享内存**,采用**单线程顺序处理+状态封闭**的设计理念。以下是其实现线程安全的详细原理: --- ### 一、三大核心设计原则 1. **隔离性(Isolation)** - 每个Actor拥有私有内存状态,不与其他Actor共享 - 类比:现实中的邮局系统,每个工作人员(Actor)独立处理自己柜台的事务 2. **消息驱动(Message Passing)** - 通信仅通过发送**不可变消息**实现 - 类比:信件投递,发送后原信件内容不会改变 3. **顺序处理(Single-threaded Processing)** - 每个Actor内部采用单线程处理消息队列 - 类比:银行柜台叫号系统,逐个处理客户请求 --- ### 二、具体实现机制 #### 1. 消息队列(Mailbox) ```text +----------------+ 异步投递 +-----------------+ | Actor A | -----------------> | Actor B | | | 不可变消息 | Mailbox: [m1,m2]| +----------------+ +-----------------+ ``` - 每个Actor拥有独立的消息队列(Mailbox) - 消息按FIFO顺序处理(特定模式可调整顺序) #### 2. 处理流程 ```python while True: message = mailbox.pop() # 从队列取消息 process(message) # 处理消息(单线程) update_state() # 修改私有状态 ``` - 严格单线程处理:同一时刻仅处理一个消息 - 状态修改无需加锁:处理过程中无并发访问 #### 3. 消息不可变性 ```java // 正确示例:发送不可变对象 actor.tell(new ImmutableMessage("data")); // 错误示例:发送可变对象(破坏线程安全) MutableObject obj = new MutableObject(); actor.tell(obj); obj.modify(); // 其他Actor可能同时读取到被修改的状态 ``` --- ### 三、与传统并发模型对比 | 特性 | Actor模型 | 传统多线程模型 | |-------------------|-----------------------------|-----------------------| | 状态存储 | 分布式私有状态 | 共享内存 | | 通信方式 | 异步消息传递 | 直接内存访问 | | 同步机制 | 无锁(通过队列顺序处理) | synchronized/Lock等 | | 错误隔离 | 崩溃不影响其他Actor | 线程崩溃影响整个进程 | | 扩展性 | 天然支持分布式 | 需要复杂同步机制 | --- ### 四、典型应用场景 1. **高并发系统**:如即时通讯服务的消息路由 2. **分布式系统**:Akka Cluster实现跨节点通信 3. **游戏服务器**:每个游戏实体作为独立Actor 4. **事务处理系统**:银行转账等需要强隔离性的场景 --- ### 五、优势与局限 **优势**: - 天然避免死锁(无锁设计) - 更易实现高并发(每个Actor≈轻量级线程) - 故障隔离(一个Actor崩溃不影响整体系统) **局限**: - 消息传递存在序列化开销 - 调试难度较高(异步流程跟踪复杂) - 不适用于需要强一致性的场景 通过这种设计,Actor模型在保证线程安全的同时,提供了高度可扩展的并发解决方案。当代框架如Akka(Java/Scala)、Orleans(.NET)、Erlang OTP等都基于此模型实现。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值