文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

很久以前,为了在 Java 中运行并行代码,人们必须手动启动新线程。这不仅仅写起来困难,还很容易产生难以查找的缺陷。测试、阅读以及维护这种代码也毫不轻松。那之后,随着振奋人心的多核时代到来,Java API 产生不小的改进,使并发编程变得容易起来。同时,其它 JVM 语言也各自发挥,帮助开发者编写并发代码。本文中我会对 Java 和 Kotln 实现并发编程进行比较。

为了不偏离重心,为了代码的可读性,我故意忽视了代码的性能。

第 1 段(可获 1.44 积分)

用例

这个用例并不太古老。我们需要调用不同的 Web 服务。原始的解决办法是按顺序对其进行逐个调用,同时获取它们的每个结果。这种情况下,总的调用时间会是调用各个服务时间的总和。可以简单地对其进行改良,对这些服务采用并行调用,等待直到最后一个完成。这样,其性能就会从线性增长变为恒定 —— 或者更学术化地说,从 O(n) 到 O(1)。

 

第 2 段(可获 1.18 积分)

为了模拟调用 Web 服务的延迟,我们使用下面的代码 (为了简洁,使用用 Kotlin 编写):

class DummyService(private val name: String) {
    private val random = SecureRandom()
    val content: ContentDuration
    get() {
        val duration = random.nextInt(5000)
        Thread.sleep(duration.toLong())
        return ContentDuration(name, duration)
    }
}
data class ContentDuration(val content: String, val duration: Int)

Java 的 Future API

Java 提供了完整的类层次结构来处理并发调用。它基于以下这些类:

 

第 3 段(可获 0.59 积分)

CallableCallable 是“具有返回值的任务”。换个角度来看,它就像是一个没有参数但有返回值的函数。

FutureFuture 表示“异步运算的结果”。它还“只能在计算完成后通过 get 方法获取结果,必要时会产生阻塞直到完成准备”。换个说法就是它对某值进行封装,而这个值是某个计算的结果。

Executor ServiceExecutorService “提供管理终端的方法,以及产生 Future 来跟踪一个或多个异步任务过程的方法”。它是 Java 中处理并发代码的入口。我们可以通过 Executors 类的静态方法获得这个接口的实现甚至是特殊实现。

第 4 段(可获 1.66 积分)

下图展示了一个概况:

Image title

使用 concurrent 包来调用服务只需要两步。

 

创建 Callable 集合

首先需要一个 Callable 集合,它会被传递给执行服务(ExecutorService)。其步骤是:

  1. 产生一个包含服务名称的流
  2. 使用每个服务名称创建命名的服务
  3. 返回每个服务对象的 getContent() 方法引用,作为 Callable。可以这样做是因为该方法签名与 Callable.call() 匹配,而且 Callable 是一个函数式接口。

这一步是准备阶段,它可以用下面的代码表示:

第 5 段(可获 1.28 积分)
​List<Callable<ContentDuration>> callables = Stream.
    of(“Service A”, “Service B”, “Service C”)
        .map(DummyService::new)
        .map(service
            -> (Callable<ContentDuration>) service::getContent)
        .collect(Collectors.toList());

处理 Callable

列表准备好之后,就该让 ExecutorService 来处理它了,这被称为“真正的工作”。

  1. 创新新的执行服务 —— 任意一个
  2. 将 Callable 列表传递给执行服务,这会得到一个 Future 流。
  3. 返回每一个 Future 对象的结果,或者处理其异常。
第 6 段(可获 0.71 积分)

下面是一种实现:

ExecutorService executor = Executors.newWorkStealingPool();
List < ContentDuration > results = executor.
invokeAll(callables).stream()
    .map(future - > {
        try {
            return future.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }).collect(Collectors.toList());

在 Kotlin 中使用 Future API

我们得面对一个事实:使用 Java 编写并发代码,即不好阅读,也不好操作。其主要原因包括:

 

第 7 段(可获 0.46 积分)
  • 需要在集合和流之间往返操作。
  • 需要在 Lambda 表达式中处理异常。
  • 需要显示转换类型。
var callables: List < Callable < ContentDuration >> =
    arrayOf(“Service A”, “Service B”, “Service C”)
    .map {
        DummyService(it)
    }
    .map {
        Callable < ContentDuration > {
            it.content
        }
    }
val executor = Executors.newWorkStealingPool()
val results = executor.invokeAll(callables).map {
    it.get()
}

Kotlin 协程

Kotlin 1.1 版本带来了称为协程的新特性。下面这段文字来自 Kotlin 的文档

 

第 8 段(可获 0.43 积分)

“基本上可以将协程看作可以被挂起,不阻塞线程的计算。阻塞线程代价昂贵,尤其是在高负荷的情况下 [...]。而另一方面,协程的挂起几乎没有代价。既没有上下文切换,也不会跟 OS 有其它相关的牵连。”

协程有着领先的设计原则,它们看起来是顺序的,但执行起来就像并行代码一样。它们基于这个理想,没有什么比代码本身更有效。接下来我们用 Kotlin 的协程替换掉 Java 的 Future 重新实现上面的内容。

第 9 段(可获 1.13 积分)

在这之前,我们先来扩展服务,添加一个新的计算属性,它是一个封装了内容的 Deferred。它会让后续步骤变得更简单:

val DummyService.asyncContent: Deferred<ContentDuration>
    get() = async(CommonPool) { content }

这段代码是标准的 Kotlin 扩展属性代码,请注意其中的 CommonPool 参数。这是一个让代码并发执行的魔法。它是一个伴生对象(比如单例就是),使用多回退(multi-fallback)算法来获取 ExecutorService实例。

比较合适的代码流程如下:

  1. 在块中处理处理协程。在块外面声明一个可变长度的列表,并在块内对其赋值。
  2. 开始同步块。
  3. 创建服务名称数组。
  4. 返回根据每个名称创建的服务。
  5. 返回每个服务的异步内容(在上面声明的 asyncContent)。
  6. 获取每个 Deferred 对象的结果。
第 10 段(可获 1.63 积分)
// Variable must be initialized or the compiler complains
// And the variable cannot be used afterwards
var results: List<ContentDuration>? = null
runBlocking {
    results = arrayOf(“Service A”, “Service B”, “Service C”)
        .map { DummyService(it) }
        .map { it.asyncContent }
        .map { it.await() }
}

后记

Future API 不像 Java 语言本身有这么多问题。其代码翻译成 Kotlin 代码之后,可读性会显著提升。然而创建集合并传递给执行服务会破坏原来良好的函数式管道。对于协程来说,唯一不爽的是需要从 var 迁移到 val 以获得最终结果(或者把结果添加到可变列表)。

还得提醒一下,协程还在实验阶段。尽管如此,代码确实看起来是顺序的 —— 但它确实兼具可读性和并行行为能力。本文完整的源代码以 Maven 格式提供,可以在 Github 上找到。

 

第 11 段(可获 1.43 积分)

文章评论