akka共享内存
Akka共享内存
Akka中的共享内存是基于Actor模型的,Actor模型提倡的是:通过通讯来实现共享内存,而不是用共享内存来实现通讯,这点是跟Java解决共享内存最大的区别,举个例子:
在Java中我们要去操作共享内存中数据时,每个线程都需要不断的获取共享内存的监视器锁,然后将操作后的数据暴露给其他线程访问使用,用共享内存来实现各个线程之间的通讯,而在Akka中我们可以将共享可变的变量作为一个Actor内部的状态,利用Actor模型本身串行处理消息的机制来保证变量的一致性。
当然要使用Akka中的机制也必须满足一下两条原则:
- 消息的发送必须先于消息的接收
- 同一个Actor对一条消息的处理先于下一条消息处理
第二个原则很好理解,就是上面我们说的Actor内部是串行处理消息,那我们来看看第一个原则,为什么要保证消息的发送先于消息的接收,是为了防止我们在创建消息的时候发生了不确定的错误,接收者将可能接收到不正确的消息,导致发生奇怪的异常。
通过前面的学习我们知道Actor是一种比线程更轻量级,抽象程度更高的一种结构,它帮我们规避了我们自己去操作线程,那么Akka底层到底是怎么帮我们去保证共享内存的一致性的呢?
一个Actor它可能会有很多线程同时向它发送消息,之前我们也说到Actor本身是串行处理的消息的,那它是如何保障这种机制的呢?
Mailbox
Mailbox在Actor模型是一个很重要的概念,我们都知道向一个Actor发送的消息首先都会被存储到它所对应的Mailbox中,那么我们先来看看MailBox的定义结构(本文所引用的代码都在akka.dispatch.Mailbox.scala中,有兴趣的同学也可以去研究一下):
private[akka] abstract class Mailbox(val messageQueue: MessageQueue)
extends ForkJoinTask[Unit] with SystemMessageQueue with Runnable {}
很清晰Mailbox内部维护了一个messageQueue这样的消息队列,并继承了Scala自身定义的ForkJoinTask任务执行类和我们很熟悉的Runnable接口,由此可以看出,Mailbox底层还是利用Java中的线程进行处理的。那么我们先来看看它的run方法:
override final def run(): Unit = {
try {
if (!isClosed) { //Volatile read, needed here
processAllSystemMessages() //First, deal with any system messages
processMailbox() //Then deal with messages
}
} finally {
setAsIdle() //Volatile write, needed here
dispatcher.registerForExecution(this, false, false)
}
}
为了配合理解,我们这里先来看一下定义:
@inline
final def currentStatus: Mailbox.Status = Unsafe.instance.getIntVolatile(this, AbstractMailbox.mailboxStatusOffset)
@inline
final def isClosed: Boolean = currentStatus == Closed
这里我们可以看出Mailbox本身会维护一个状态Mailbox.Status,是一个Int变量,而且是可变的,并且用到volatile来保证了它的可见性:
@volatile
protected var _statusDoNotCallMeDirectly: Status = _ //0 by default
现在我们在回去看上面的代码,run方法的执行过程,首先它会去读取MailBox此时的状态,因为是一个Volatile read,所以能保证读取到的是最新的值,然后它会先处理任何的系统消息,这部分不需要我们太过关心,之后便是执行我们发送的消息,这里我们需要详细看一下processMailbox()的实现:
@tailrec private final def processMailbox(
left: Int = java.lang.Math.max(dispatcher.throughput, 1),
deadlineNs: Long = if (dispatcher.isThroughputDeadlineTimeDefined == true) System.nanoTime + dispatcher.throughputDeadlineTime.toNanos else 0L): Unit =
if (shouldProcessMessage) {
val next = dequeue() //去出下一条消息
if (next ne null) {
if (Mailbox.debug) println(actor.self + " processing message " + next)
actor invoke next
if (Thread.interrupted())
throw new InterruptedException("Interrupted while processing actor messages")
processAllSystemMessages()
if ((left > 1) && ((dispatcher.isThroughputDeadlineTimeDefined == false) || (System.nanoTime - deadlineNs) < 0))
processMailbox(left - 1, deadlineNs) //递归处理下一条消息
}
}
从上述代码中我们可以清晰的看到,当满足消息处理的情况下就会进行消息处理,从消息队列列取出下一条消息就是上面的dequeue()
,然后将消息发给具体的Actor进行处理,接下去又是处理系统消息,然后判断是否还有满足情况需要下一条消息,若有则再次进行处理,可以看成一个递归操作,@tailrec
也说明了这一点,它表示的是让编译器进行尾递归优化。
现在我们来看一下一条消息从发送到最终处理在Akka中到底是怎么执行的,下面的内容是我通过阅读Akka源码加自身理解得出的,这里先画了一张流程图:
消息的大致流程我都在图中给出,还有一些细节,必须序列化消息,获取状态等就没有具体说明了,有兴趣的同学可以自己去阅读以下Akka的源码,个人觉得Akka的源码阅读性还是很好的,比如:
- 基本没有方法超过20行
- 不会有过多的注释,但关键部分会给出,更能加深自己的理解
当然也有一些困扰,我们在不了解各个类,接口之间的关系时,阅读体验就会变得很糟糕,当然我用IDEA很快就解决了这个问题。
我们这里来看看关键的部分:Actor是如何保证串行处理消息的?
上图中有一根判定,是否已有线程在执行任务?我们来看看这个判定的具体逻辑:
@tailrec
final def setAsScheduled(): Boolean = { //是否有线程正在调度执行该MailBox的任务
val s = currentStatus
/*
* Only try to add Scheduled bit if pure Open/Suspended, not Closed or with
* Scheduled bit already set.
*/
if ((s & shouldScheduleMask) != Open) false
else updateStatus(s, s | Scheduled) || setAsScheduled()
}
从注释和代码的逻辑上我们可以看出当已有线程在执行返回false,若没有则去更改状态为以调度,直到被其他线程抢占或者更改成功,其中updateStatus()是线程安全的,我们可以看一下它的实现,是一个CAS操作:
@inline
protected final def updateStatus(oldStatus: Status, newStatus: Status): Boolean =
Unsafe.instance.compareAndSwapInt(this, AbstractMailbox.mailboxStatusOffset, oldStatus, newStatus)
到这里我们应该可以大致清楚Actor内部是如何保证共享内存的一致性了,Actor接收消息是多线程的,但处理消息是单线程的,利用MailBox中的Status来保障这一机制。
总结
通过上面的内容我们可以总结出以下几点:
- Akka并不是说用了什么特殊魔法来保证并发的,底层使用的还是Java和JVM的同步机制
- Akka并没有使用任何的锁机制,这就避免了死锁的可能性
- Akka并发执行的处理并没有使用线程切换,不仅提高了线程的使用效率,也大大减少了线程切换消耗
- Akka为我们提供了更高层次的并发抽象模型,让我们不必关心底层的实现,只需着重实现业务逻辑就行,遵循它的规范,让框架帮我们处理一切难点吧
作者:三分青年
链接:https://www.jianshu.com/p/36eca0696940
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
akka共享内存的更多相关文章
- Akka系列(四):Akka中的共享内存模型
前言...... 通过前几篇的学习,相信大家对Akka应该有所了解了,都说解决并发哪家强,JVM上面找Akka,那么Akka到底在解决并发问题上帮我们做了什么呢? 共享内存 众所周知,在处理并发问题上 ...
- Linux 共享内存详解一
共享内存段被多个进程附加的时候,如果不是所有进程都已经调用shmdt,那么删除该共享内存段时,会出现一个临时的不完整的共享内存段(key值是0),无法彻底删除.只有当所有进程都调用shmdt,这个临时 ...
- PHP进程通信基础——信号量+共享内存通信
PHP进程通信基础--信号量+共享内存通信 由于进程之间谁先执行并不确定,这取决于内核的进程调度算法,其中比较复杂.由此有可能多进程在相同的时间内同时访问共享内存,从而造成不可预料的错误.信号量这个名 ...
- C++ 共享内存 函数封装
#pragma once #include <string> #include <wtypes.h> #include <map> using namespace ...
- Linux学习笔记(14)-进程通信|共享内存
在Linux中,共享内存是允许两个不相关的进程访问同一个逻辑内存的进程间通信方法,是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式. 不同进程之间共享的内存通常安排为同一段物理内存.进程可 ...
- linux 共享内存 shmat,shmget,shmdt,shmctl
shmget int shmget(key_t key, size_t size, int flag);//开辟一段共享内存 key_t key :标识符的规则() size_t size :共享内存 ...
- Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
下面将讲解进程间通信的另一种方式,使用共享内存. 一.什么是共享内存 顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存.共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式 ...
- linux后台查看共享内存和消息队列的命令
ipcs ipcs -q : 显示所有的消息队列 ipcs -qt : 显示消息队列的创建时间,发送和接收最后一条消息的时间 ipcs -qp: 显示往消息队列中放消息和从消息队列中取消息的进程ID ...
- c++共享内存(转载)
对于连个不同的进程之间的通信,共享内存是一种比较好的方式,一个进程把数据发送到共享内存中, 另一个进程可以读取改数据,简单记录一下代码 #define BUF_SIZE 256 TCHAR szNam ...
随机推荐
- Java中的Map List Set等集合类
一.概述 二 set map list的区别 三. Collections类和Collection接口 四. List接口,有序可重复的集合 五. Set接口,代表无序,不可重复的集合 六. Map接 ...
- 【Selenium-WebDriver自学】Selenium-IDE模式匹配(六)
==================================================================================================== ...
- android TextView 例子代码(文字中划线、文字下划线)
XML: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android ...
- 自写-自动拨号测试app
XML - Main <?xml version="1.0" encoding="utf-8"?> <android.support.cons ...
- Ruby学习笔记5: 动态web app的建立 (2)
上一节里,我们搭建了一个数据库的结构,并用index验证了request-response cycle,如下图: 1. Add show method into Controller 这一节,我们要继 ...
- IntelliJ IDEA 快捷键积累
Windows idea 平时常用快捷键 一.视图查看 Ctrl+F12 查看file,method结构图.类继承机构图(方法,参数,返回值) Ctrl+shift+Al ...
- IntelliJ IDEA 添加junit插件
一.使用idea做junit测试需要添加junit插件 1.安装插件 File-->settings-->Plguins-->Browse repositories-->输入J ...
- 阿里云 putty链接服务器出现 server refused our key
阿里云 putty链接服务器出现 server refused our key 创建了密钥对绑定实例,puttygen生成ppk,putty配置参数,连接,一步一步来的,结果出现 server ref ...
- 18.os模块获取url后缀
针对文件下载文件重命名文件后缀的获取,此方式只适用url里有后缀的情况(其它的要根据情况去用类似方式获取字段后缀). import os A="http://www.铜陵市建筑工程信息网.c ...
- tomcat gzip
----------- HTTP 压缩可以大大提高浏览网站的速度,它的原理是,在客户端请求网页后,从服务器端将网页文件压缩,再下载到客户端,由客户端的浏览器负责解压缩并浏览.相对于普通的浏览过程HTM ...