并发是同一时间应对多件事情的能力;并行是同一时间动手做多件事情的能力。
线程与锁模型其实是对底层硬件运行过程的形式化。
互斥:使用锁保证某一时间仅有一个线程可以访问数据;会带来竞态条件和死锁。
诡异的乱序执行:
编译器的静态优化可以打乱代码的执行顺序JVM 的动态优化可以打乱代码的执行顺序硬件可以通过乱序执行来优化性能偶然发生的死锁
一个线程使用了多把锁,就要考虑死锁发生的可能性。 简单的规则可以避开死锁:总是按照一个全局固定的顺序获取多把锁。(可以使用对象的散列值,但是散列值并不能保证唯一性) CopyOnWrite:就算只使用了一个锁,在调用未知的外部方法时,也有可能会引入额外的锁而产生死锁。 在调用外部方法时,可以考虑对外部对象实例进行复制。复制会产生额外的开销,但是也会减少外部锁冲突忙等待的开销。多线程的难点不在于难以开发,而在于难以测试。同一问题难以复现,尽可能记录完善的日志,以提供信息。
函数式编程没有可变状态,所以不会遇到共享状态带来的种种问题。函数式编程是将计算过程抽象成表达式求值,由纯数学函数构成,很容易做到线程安全。
Clojure 没有尾调用消除,因此递归调用会发生堆栈溢出。
可变状态的风险:
可变状态可能隐藏在外部类或外部方法中。可变状态可能会通过调用方法的返回值逃逸到没有加锁的外部。fold(类似 Fork/Join)创建了一个并行任务的处理树,化简操作在树的叶节点进行,直到树的根节点。
描述函数式代码执行方式的一种方法就是不断用函数的执行结果替换函数的调用。与 Java 不同的是,函数式编程的每个函数都具有引用透明性,可以安全地调整函数的求值顺序。
数据流式编程:future-promise(基于引用透明性)
对 future 的解引用将被阻塞,直至其值被计算出来。调用 promise 的代码将不会执行,直到其值被赋值,并且对 promise 的解引用也会阻塞直至其值被计算出来。函数式编程最大的好处是我们可以确信程序是按照我们预想的方式运行的。比起等价的命令式程序,函数式程序会更简单,更容易推理,也更便于测试。由于函数式程序的引用透明性,且不使用可变状态,我们可以轻松地实现并行化,且并不存在线程与锁模型中的各种 bug。(Java 的 lambda 表达式和 Steam API)
混合函数式编程和可变状态,平衡两者的优点,成为并发编程的利器。
原子变量是一个持久化数据结构,如果一个线程引用了持久化数据结构,那么其他线程对数据结构的修改对该线程就是不可见的。
持久化数据并不是保存到磁盘或数据库中,而是数据被修改时总是保留其之前的版本。因为持久化数据结构分离了标识和状态,可以看作 CopyOnWrite。在命令式语言中,一个变量混合了标识和状态,一个标识对应一个状态,而状态随时间变化。持久化数据结构将标识和状态分离开,即如果获取了一个标识的当前状态,无论这个标识怎样改变,获取的状态将不会改变。Clojure 中带!的函数不是事务安全的,
原子变量:对一个值进行隔离的,同步的更新,即在swap!返回时完成更新,多个原子变量不能一致地更新代理:对一个值进行隔离的,异步的更新,即更新可能在send返回后进行,多个代理不能一致地更新引用:对多个值进行一致的,同步的更新,使用事务alter,具有原子性,一致性和隔离性,不具有持久性涉及多个值的一致性更新时:
可以使用多个引用并通过事务来保证访问一致性。可以将多个值整个到一个数据结构中并使用原子变量来管理数据的访问一致性。Clojure 精心设计了用于并发的语义,从而保留了共享可变状态。
优点:持久化数据结构将可变量的表示与状态分离开,解决了使用锁的方案的大部分缺点。缺点:不支持分布式编程。Actor 模型保留了可变状态,只是不进行共享。Actor 对象是一个比线程还要轻量级的“进程”,通常不需要依赖线程池技术。
将错误处理隔离到一个管理进程当中,进程的异常终止通过连接进行传播,将系统转化为系统进程后,可以捕获另一个进程的终止消息,而不是和其他进程一样一起异常终止。 当其他进程异常崩溃时,管理进程会重启其他进程。虽然会丢失一部分数据,但是至少还是可用的。
如何保证消息必达:
如果没有异常发生,消息一定能被送达并被处理。如果某个环节出现异常,异常一定会通知到使用者。(容错性的基石)任其崩溃的哲学:
代码简洁且容易理解,可以清晰区分“一帆风顺”的代码和容错代码多个 Actor 独立,并不共享状态管理者可以选择是否处理崩溃分布式管理库:OTP
Actor 模型的重点在于参与交流的实体;CSP 模型的重点在于用于交流的通道。 消息传递系统决定其特性和功能的首要因素并不是用于传递消息的代码或者消息的内容,而是消息的传输通道。
ClojureScript 可以编译成 JavaScript,这样客户端变成就可以在单线程 JavaScript 上运行协作式多任务,还能解决“回调困境”。
将基于回调的代码转换成了基于 channel 的代码。
Actor 模型侧重于容错性和分布式,而 CSP 模型侧重于效率和代码表达的流畅性。
基于图形处理器的通用计算(General-Purpose computing on the GPU)
OpenCL 程序:
创建上下文,获取设备信息,创建命令队列,内核和命令队列都运行在上下文中编译内核,将内核编译成可以在设备上运行的代码创建缓冲区,内核使用的都是缓冲区中的数据,并将数据读入到输入缓冲区设置内核参数,执行工作项,让每个工作项都运行一次内核程序获取结果,并清理现场OpenCL 也适用于 CPU:Intel 处理器的流式 SIMD 指令集和多核 CPU。
Java + LWJGL(LightWeight Java Graphics Library) 封装了 OpenCL 和 OpenGL。
小规模应用数据并行技术。Lambda 架构既使用了可以进行大规模数据批处理的 MapReduce 技术,可使用了可以快速处理数据并及时反馈的流处理技术,这样的混搭能够为大数据问题提供扩展性、响应性和容错性都很优秀的解决方案。这里侧重于 Lambda 的并发和分布式特性。 Lambda 架构源自于它与函数式编程的相似性。从本质上来说,Lambda 架构是将计算函数施加于大量数据的一种通用方法。
将算法拆分成映射和化简两步的一个好处是易于并行化。
词频统计:
将输入分配给多个 mapper,每个 mapper 都会产生一些键值对。这些键值对会被发送给 reducer,产生最终的输出(通常也是一系列键值对)。每个 reducer 对应的键是不同的,因此具有相同键的键值对都会发送给同一个 reducer 进行处理。信息分为原始数据和(源于原始数据的)衍生信息。原始数据是永恒的真相,也是 lambda 架构的基础。
原始数据是永远不变的,如果改变,可以添加时间戳。如果能够准确预测出未来会对原始数据进行怎样的查询,就可以预结算出一个批处理视图,这个视图包含这些查询将要返回的衍生信息,或者可以计算出这些衍生信息的数据。(例如:对历史数据生成报表和进行分析)
批处理层不能单独构成端到端的应用,还需要服务层,对生成的批处理视图进行索引,这样就可以对索引进行查询了。(可以使用传统数据库来构建服务层)
由于批处理层需要一定的时间来进行处理和更新,因此保存的并不是最新的数据。
加速层用于解决批处理层具有较高延迟的代价问题。当有新数据产生时,一方面将其添加到原始数据中,这样批处理层就可以进行处理;另一方面将其传给加速层。加速层会生成实时视图,实时视图会和批处理视图合并来满足对最新数据的查询。实时视图仅包含最后一次生成批处理视图后产生的原始数据所对应的衍生信息,当这部分数据被批处理层处理后,该实时视图将被弃用。
加速层的过期和交替使用技术。通过维护两个加速层来达到更好的性能和可靠性。
加速层的构建可以是同步的或异步的。
Storm(Clojure)实现异步加速层。Hadoop 实现批处理,Storm 实现实时处理,方便地使用多台计算机进行分布式计算,以改善性能和容错性。
Spark 可以使用很多比 MapReduce 更自然的算法(例如:图算法),批处理层和加速层也可以用 Spark 来实现。