并发

2018-02-24 15:48 更新

现代服务是高度并发的—— 服务器通常是在10–100秒内并列上千个同时的操作——处理隐含的复杂性是创作健壮系统软件的中心主题。

线程提供了一种表达并发的方式:它们给你独立的,堆共享的(heap-sharing)由操作系统调度的执行上下文。然而,在Java里线程的创建是昂贵的,是一种必须托管的资源,通常借助于线程池。这对程序员创造了额外的复杂,也造成高度的耦合:很难从所使用的基础资源中分离应用逻辑。

当创建高度分散(fan-out)的服务时这种复杂度尤其明显: 每个输入请求导致一大批对另一层系统的请求。在这些系统中,线程池必须被托管以便根据每一层请求的比例来平衡:一个线程池的管理不善会导致另一个线程池也出现问题。

一个健壮系统必须考虑超时和取消,两者都需要引入更多“控制”线程,使问题更加复杂。注意若线程很廉价这些问题也将会被削弱:不再需要一个线程池,超时的线程将被丢弃,不再需要额外的资源管理。

因此,资源管理危害了模块化。

Future

使用Future管理并发。它们将并发操作从资源管理里解耦出来:例如,Finagle(译注:twitter的一个RFC框架)以有效的方式在少量线程上实现并发操作的复用。Scala有一个轻量级的闭包字面语法(literal syntax),所以Futures引入了很少的语法开销,它们成为很多程序员的第二本能。

Futures允许程序员用一种可扩充的,有处理失败原则的声明风格,来表达并发计算。这些特性使我们相信它们尤其适合在函数式编程中用,这也是鼓励使用的风格。

更愿意转换(transforming)future而非自己创造。Future的转换(transformations)确保失败会传播,可以通过信号取消,对于程序员来说不必考虑Java内存模型的含义。甚至一个仔细的程序员会写出下面的代码,顺序地发出10次RPC请求而后打印结果:

 val p = new Promise[List[Result]]
 var results: List[Result] = Nil
 def collect() {
   doRpc() onSuccess { result =>
     results = result :: results
     if (results.length < 10)
       collect()
     else
       p.setValue(results)
   } onFailure { t =>
     p.setException(t)
   }
 }

 collect()
 p onSuccess { results =>
   printf("Got results %s\n", results.mkString(", "))
 }

程序员不得不确保RPC失败是可传播的,代码散布在控制流程中;糟糕的是,代码是错误的! 没有声明results是volatile,我们不能确保results每次迭代会保持前一次值。Java内存模型是一个狡猾的野兽,幸好我们可以通过用声明式风格(declarative style)避开这些陷阱:

 def collect(results: List[Result] = Nil): Future[List[Result]] =
   doRpc() flatMap { result =>
     if (results.length < 9)
       collect(result :: results)
     else
       result :: results
   }

 collect() onSuccess { results =>
   printf("Got results %s\n", results.mkString(", "))
 }

我们用flatMap顺序化操作,把我们处理中的结果预追加(prepend)到list中。这是一个通用的函数式编程习语的Futures译本。这是正确的,不仅需要的样板代码(boilerplate)可以减少,易出错的可能性也会减少,并且读起来更好。

Future组合子(combinators)的使用。当操作多个futures时,Future.select,Future.join和Future.collect应该被组合编写出通用模式。

集合

并发集合的主题充满着意见、微妙(subtleties)、教条、恐惧/不确定/怀疑(FUD)。在大多实际场景都不存在问题:总是先用最简单,最无聊,最标准的集合解决问题。 在你知道不能使用synchronized前不要去用一个并发集合:JVM有着老练的手段来使得同步开销更小,所以它的效率能让你惊讶。

如果一个不可变(immutable)集合可行,就尽可能用不可变集合——它们是指称透明的(referentially transparent),所以在并发上下文推断它们是简单的。不可变集合的改变通常用更新引用到当前值(一个var单元或一个AtomicReference)。必须小心正确地应用:原子型的(atomics)必须重试(retried),变量(var类型的)必须声明为volatile以保证它们发布(published)到它们的线程。

可变的并发集合有着复杂的语义,并利用Java内存模型的微妙的一面,所以在你使用前确定你理解它的含义——尤其对于发布更新(新的公开方法)。同步的集合同样写起来更好:像getOrElseUpdate操作不能够被并发集合正确的实现,创建复合(composite)集合尤其容易出错。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号