接下来打算花一些篇幅介绍一些异步编程的范式,也算是给自己一个学习笔记吧。

异步编程是个很诡异的领域,每个人都在谈论它,但是工作中很少有人能驾驭;很多很新潮很热的异步编程概念,运用起来却完全达不到它宣称的效率提升,甚至不拖后腿就不错。

理想的异步编程模型,应该是像同步逻辑一样编码,透明且并行的运行。但是这个并不很现实,虽然这可以说是很多函数式编程语言的研究方向和卖点,但是完全透明的将同步代码异步化,保持并发安全,甚至并行化,是非常难的问题,更何况我们还做不到让编译器区分程序代码中的时序和因果。

醒醒吧,人都不一定能区分。

退而求其次,好的模型应该允许在尽量不侵入代码风格情况下,提供并发安全的异步编程模型,应该能够并行化,能够比较容易的分布,对度量和剖分友好。

这也是我近几年开始从Python转向JVM平台的主要动力。

当然,有些在时序上就是并行的程序逻辑,强调无感并不是好事。在这种情况下,能清晰的表达出异步行为,提供安全的协作方式,就是很好的技术。

Java标准库提供的并发技术,从Thread层面到各种异步任务模式都有,好处是都不太复杂,不足是都不够灵巧。可以说是Java的老传统。

Clojure天然对异步比较友好,它的基本类型,包括map、vector、list这些,都是不可变的,又提供了ref、atom、agent这些数据类型用于状态传递。但是如果不配合 core.async 使用的话,仅标准库里内置的东西,还是比较简陋的。

Clojure 的 core.async 库提供了足够多的工具,不至于复杂到难以驾驭,但是已经完备到可以支撑一个正经的项目对并行和异步的需要。拿它当做Java的并行框架,把Clojure当作一个DSL来使用也很好。如果贪心不足一些,一定要说什么不满意的地方。嗯,这里面没有分布式什么事儿。

有个 framework 叫 onyx ,Clojure 的坚定支持者可以关注一下。不过我个人比较倾向这部分用java解决。

AKKA 本身是用 Scala 编写的,但是它为 Java 提供完整的调用接口,那么也就向 Clojure 开放了大门。

相对来说,core.async 的风格更友善和简单,基本上可以归结为 wait/loop 模式的异步任务和用来传递消息的队列式接口,并支持多种传播模式(1:1 到 n:m 都有)。而 Akka 侵入性更强一些,它要求我们把异步任务封装在 actor 中,用actor来管理状态,完成协作。但是在实际使用中,akka对其它框架也非常友好,基本上我们只需要把 akka 负责的那部分逻辑封装好就可以,几乎不会担心出现意外的死锁和阻塞。

而且,Akka 的分布非常的平滑和友善,哪怕只是为了这个分布式框架,也值回票价了。

不过如果直接在clojure里写 actor,并不方便,我在经过一些简单尝试以后,很快放弃了这种做法。主要是在Clojure里定义一个 Java 类,本身是个比较啰嗦的事情,如果要遵循 AKKA 的规范用 Props 封装就更啰嗦(参见 How to generate generate static methods with clojure's Gen-class? )

我们简单点儿,先写个leiningen项目,我在 Github 上建了一个示例项目 MarchLiu/market ,它会是多个 project 的集合,用过 Eclipse 的同行对 workshop 的概念不会陌生。如果您和我一样是 Intellij 用户么,也不会多麻烦,我是在本地用 Intellij 打开这个项目目录,然后删掉它为我自动生成的 market 模块,然后添加了一个名为 sequences 的 leiningen 新模块。

此后的内容中,我默认大家已经安装了 java 11 并使用这个版本的语法。

现在我们编辑seqences目录下的project.clj

(defprojectsequences "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:source-paths ["src/main/clojure"]
:java-source-paths ["src/main/java"]
:dependencies [[org.clojure/clojure "1.9.0"]
[com.typesafe.akka/akka-actor_2.12 "2.5.19"]]
:aot :all
:main liu.mars.market.App)
然后在 src/main/java/liu/mars/market 下面建立 Sequences.java :
package liu.mars.market;
import akka.actor.AbstractActor;
import akka.actor.Props;
import akka.japi.pf.ReceiveBuilder;
public class SequencesActor extends AbstractActor {
private long orderId = 0;
public static Props props() {
return Props.create(SequencesActor.class, SequencesActor::new);
}
@Override
public Receive createReceive() {
return ReceiveBuilder.create().matchEquals("next", msg -> {
sender().tell(++orderId, self());
}).build();
}
}

这个类型演示了一个 Actor 必要的组成元素:它暴露给 AKKA 框架的构造逻辑,和消息响应逻辑。现在这个版本的逻辑非常简单,收到一个 "next" 字符串后,它就向 sender 返回当前 orderId 的自增结果。

现在我们写一个调用逻辑,看看运行效果,我们现在建立 liu.mars.App 类型:

package liu.mars.market;
import akka.actor.AbstractActor;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import akka.japi.pf.ReceiveBuilder;
public class App extends AbstractActor {
private LoggingAdapter log = Logging.getLogger(this.context().system(), this.getClass());
@Override
public Receive createReceive() {
return ReceiveBuilder.create().match(Long.class, msg -> {
log.info("received long: {}", msg);
}).build();
}
public static void main(String[] args){
ActorSystem system = ActorSystem.create("sequences");
var seqRef = system.actorOf(SequencesActor.props(), "sequences");
var mineRef = system.actorOf(Props.create(App.class), "ask");
seqRef.tell("next", mineRef);
}
}

这个类型附带了 main 函数,在其中演示了构造 Akka Context System ,并使用它创建 actor ,进行消息传递的过程。简单起见,我们利用 App 类型自己作为接受序列号的 actor ,用 AKKA log 把收到的消息显示出来。

笑话时间:这个程序虽然还没什么用,但是已经可以运行了。

Actor 的异步环境切换由 Context System维护,所以每个 Actor 内部是并发安全的,所以这里我甚至没有用 Atomic 。现在我们修改 main 函数,多取一些 id。

public static void main(String[] args) throws InterruptedException {
ActorSystem system = ActorSystem.create("sequences");
var seqRef = system.actorOf(SequencesActor.props(), "sequences");
var mineRef = system.actorOf(Props.create(App.class), "ask");
while (true) {
seqRef.tell("next", mineRef);
Thread.sleep(500);
}
}

死循环如愿稳定输出了递增序列:

当然这个东西仍然没有什么用就是了,比如作为一个定序器,甚至连持久化都还没做,下次重启就还会从1开始……

所以我今儿晚上花了一些时间把去年从老东家淘来的一个二手笔记本重装了系统,后面的篇幅我们写一些更正经的程序示例。

关于 Actor 异步模型,已经不是什么冷门,网上的讨论和出版物都有很多,不过多展开。这个程序呢,暂时还体现不出异步编程的作用。不过它仍然可以展示一些有意思的细节,比如 Context System 实际上管理着多个 dispatcher 。再比如我们即使把它放在多个 Context System 中,也依然可以安全的传递消息:

package liu.mars.market;
import akka.actor.AbstractActor;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import akka.japi.pf.ReceiveBuilder;
public class App extends AbstractActor {
private LoggingAdapter log = Logging.getLogger(this.context().system(), this.getClass());
@Override
public Receive createReceive() {
return ReceiveBuilder.create().match(Long.class, msg -> {
log.info("received long: {}", msg);
}).build();
}
public static void main(String[] args) throws InterruptedException {
ActorSystem seqSys = ActorSystem.create("sequences");
ActorSystem appSys = ActorSystem.create("app");
var seqRef = seqSys.actorOf(SequencesActor.props(), "sequences");
var mineRef = appSys.actorOf(Props.create(App.class), "ask");
while (true) {
seqRef.tell("next", mineRef);
Thread.sleep(500);
}
}
}

为什么特地提到这个?因为在 Vert.x里使用多个 Vertx 很容易就会因为阻塞报错,不但一个进程内要求单一 verticle ,而且还会自动就去要 tcp 端口在网络上发送一堆莫名其妙的东西,真是货比货得扔。

当然这样仍然没什么好玩的,下一篇我们弄个 PostgreSQL ,把这个序列服务持久化。

==========================

之前写错了一处,每个进程一个的是 Vertx 对象,这个东西对应的是 AKKA 的 Context System,Verticle 更像是 actor ,是维护一致状态的。 @圆胖肿 大概觉得我黑vertx,但是这事儿吧……

我也挺失望的╮(╯▽╰)╭