七周七并发模型

第1章 概述

  • 1.1 并发还是并行
    • 并发:同一时间应对多件事情的能力;
    • 并行:同一时间动手做多件事情的能力;
  • 1.2 并行架构
    • 位级(bit-level)并行
    • 指令级(instruction-level)并行
    • 数据级(data)并行
    • 人物级(task-level)并行
  • 1.3 并发: 不只是多核
    • 并发的世界,并发的软件
    • 分布式的世界,分布式的软件
    • 不可预测的世界,容错性强的软件
    • 复杂的世界,简单的软件
  • 1.4 七个模型
    • 线程与锁
    • 函数式变成
    • Clojure之道–分离标识与状态
    • 通信顺序进程(Communicating Sequential Processes CSP)
    • 数据级并行
    • Lambda架构

第2章 线程与锁

  • 2.1 简单粗暴
  • 2.2 第一天:互斥和内存模型
    • 编译器的静态优化看打乱代码的执行顺序;
    • JVM的动态优化也会打乱代码的执行顺序
    • 硬件可以通过乱序执行来优化其性能.
    • 线程与锁模型带来的三个主要危害—竞态条件,死锁和内存可见性,可以利用如下准则避免危害:
      • 对共享变量的所有访问都需要同步化
      • 读线程和写线程都需要同步化
      • 按照约定的全局顺序来获取多把锁
      • 当持有锁时避免调用外星方法
      • 持有锁的时间应尽可能短
  • 2.3 第二天:超越内置锁
    • 内置锁的缺点:
      • 一个线程因为等待内置锁而进入阻塞之后,就无法中断该线程了。
      • 尝试获取内置锁时,无法设置超时
      • 获得内置锁,必须使用synchronized
      • 获取锁和释放锁的代码必须严格嵌在同一个方法中。另外声明synchronized的函数其实只是一个”语法糖”
synchronized (object) {
  //使用共享资源
}
//等价
synchronized (this) {
 //函数体
}
  • ReentrantLock
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
  while (!<条件为真>){
    condition.await()
  }

  //使用共享资源
} finally {
  lock.unlock();
}
//其他地方需要调用 signal()/signalAll()
  • volatile:随着JVM被不断优化,其提供了一些低开销的锁,volatile变量的适用场景也越来越少。如果你考虑使用volatile,也许应当在java.util.concurrent.atomic包中寻找更合适的工具

  • 总结:ReentrantLockjava.util.concurrent.atomic突破了使用内置锁的限制,利用新工具类可以做到:
    • 在线程获取锁时中断它;
    • 设置线程获取锁的超时时间;
    • 按任意顺序获取和释放锁;
    • 用条件变量等待某个条件变为真;
    • 使用原子变量避免锁的使用;
  • 第三天:站在巨人的肩膀上
    • java.util.concurrent包提供的工具不仅让并发编程更容易而且在以下方面让程序更安全高效:
      • 使用线程池,而不是直接创建线程
      • 使用CopyOnWriteArrayList
      • 使用ArrayBlockingQueue让生产者和消费者之前高效协作;
      • ConcurrentHashMap提供了更好的并发访问
      • ForkJoinPoll,CountDownLatch,CyclicBarrier,work-stealing

第3章 函数式编程

  • 3.2 第一天:抛弃可变状态
  • 可变状态的风险
    • 隐藏的可变状态
    • 逃逸的可变状态
  • Clojure旋风之旅:
~ clj
Clojure 1.9.0
user=> (+ 1 2)
3
user=> (max 3 4)
4
user=> (max 2 5)
5
user=> (+ 1 (* 2 3))
7
user=> (def meaning-of-life 42)
#'user/meaning-of-life
user=> meaning-of-life
42
user=> (if (< meaning-of-life 0) "negative" "non-negative")
"non-negative"
user=> (def droids ["Huey" "Dewey" "Louie"])
#'user/droids
user=> (count droids)
3
user=> (droids 0)
"Huey"
user=> (droids 2)
"Louie"
user=> (def me {:name "Paul" :age 45 :sex :male})
#'user/me
user=> (:age me)
45
user=> (:name me)
"Paul"
user=> (defn percentage [x p] (* x (/ p 100.0)))
#'user/percentage
user=> (percentage 200 10)
20.0
user=>
  • 下面举例说明函数式编程最有趣的地方时不使用可变状态:数列求和

  • Java代码

public int sum( int[] numbers) { 
  int accumulator = 0; 
  for (int n: numbers) 
    accumulator += n; 
  return accumulator; 
}
  • Clojure
//V1
(defn recursive- sum [numbers] 
  (if (empty? numbers) 
    0 
    (+ (first numbers) (recursive- sum (rest numbers)))))

//V2
(defn reduce- sum [numbers] 
  (reduce (fn [acc x] (+ acc x)) 0 numbers))

//V3
(defn sum [numbers] 
  (reduce + numbers))
  • clojure操作符特性
user=> (+)
0
user=> (*)
1
user=> (/)
ArityException Wrong number of args (0) passed to: core//  clojure.lang.AFn.throwArity (AFn.java:429)
user=> (-)
ArityException Wrong number of args (0) passed to: core/-  clojure.lang.AFn.throwArity (AFn.java:429)
user=>
  • 并行
(ns sum. core 
  (:require [clojure. core. reducers :as r])) 
(defn parallel- sum [numbers] 
  (r/fold + numbers))
//todo 性能分析

// TODO 需要先学以下Clojure然后继续阅读

第6章 通信顺序金正

第一天:channel和go块

-core.async库提供了两个主要的工具–channel和go块,在大小 有限的线程池中,go块允许多个并发任务复用线程资源。

  • channel就是一个线程安全的队列—任何任务只要持有channel的引用,就可以向一端添加消息,也可以从另外一端删除消息。在actor模型中,消息是从指定的一个actor发往指定的另一个actor的;与之不同,使用channel发送消息时发送者并不知道谁是接收者,反之亦然。
  • 默认情况下,channel是同步的(无缓存的)—-向一个channel写入数据的操作将一直被阻塞,直到另一个任务从channel中读出消息
  • channel也可以是有缓存的,在缓存区已满时可以使用不同的缓存策略,阻塞型(blocking),弃用新值型(dropping),移出旧值型(sliding)
  • 通过控制反转,go块将串型代码重写成一个状态机。go块不会进行阻塞,而是暂停状态机,这样当前所处的线程就可以为另一个go块所使用
  • channel操作的阻塞版本的函数名是以两个感叹号(!!)结尾,而暂停版本的函数名是以一个感叹号(!)结尾。注意这里说的是clojure的用法,就是clojure调用go语言的channel方式.
  • TODO

第7章 数据并行

第一天 GPGPU编程
  • GPU适合的场景举例:图形处理与数据并行;数据并行可以通过多种方法来实现,举例其中的两种:流水线和多ALU
  • 流水线
  • 多ALU:CPU中负责进行乘法之类运算的组件称为算术逻辑单元(ALU);只要搭配足够宽的内存总线,多个ALU就可以同时获取多个操作数,这样施加在大量数据上的运算就可以并行了。GPU的内存总线通常有256位或更宽,也就是说一次可以获取8个或更多个32位的浮点数。
  • 注意:为了获得更好的性能,现实的GPU会综合使用流水线,多ALU以及许多本书未提及的技术,增加了进一步理解GPU的难度,不同厂商GPU之间的共性也是很少的。
  • 利用OpenCL可以挖掘出GPU的数据并行处理的能力,并进行通用编程,从而获得很大的性能提升。
  • 通过将任务切分成工作项,OpenCL可以将任务并行化
  • 通过编写内核,指定了单个工作项是如何工作的。
  • 要执行内核,主机程序必须遵守以下步骤:
    • 1 创建上下文,内核和命令队列都将运行在这个上下文中
    • 2 编译内核
    • 3 创建输入数据的缓存区和输出数据的缓存区
    • 4 向命令队列中输入一个命令,让每一个工作项都运行一次内核程序
    • 5 获取结果
第二天 多维空间与工作组
  • 内存模型,工作项执行内核程序时,会访问四种不同的内存区域:
    • 全局内存,同一个设备上执行的所有工作项都可以使用的内存
    • 常量内存:全局内存的一部分,在执行内核时保持不变
    • 局部内存:工作组私有的内存,可用于工作组中不同工作项之间的通信
    • 私有内存:工作项私有的内存。
  • OpenCL定义了一些用于抽象底层硬件细节的概念,包括平台,执行和内存模型:
  • 工作项是在处理元件中执行的
  • 多个处理元件构成了计算单元
  • 在同一个计算单元中执行的一组工作项构成工作组
  • 一个工作组中的工作项之间通过局部内存进行通信,利用同步屏障进行数据同步,保证一致性。
  • TODO
  • 数据并行非常适用于处理大量数值数据,尤其适合于科学计算,工程计算以及仿真领域,比如流体力学,有限元分析,N体模拟,模拟退火,神经网络等等

第8章 Lambda架构

第一天 MapReduce
  • MapReduce是一个多义术语,可以指代一类算法,这类算法分为两个步骤:对一个数据结构首先进行映射(map)操作,然后进行简化(reduce)操作,一个好处就是易于并行化,可以指代一类系统,这类系统使用了上面的算法将计算过程高效地分布到一个集群上,这类系统不仅可以将数据和数据处理分布到集群的多台计算机上,还可以在一台或多台计算机崩溃时继续正常运转。
  • Lambda架构源自它与函数式编程的相似性。从本质上说,Lambda架构是将计算函数施加于大量数据的一种通用方法。
  • 将一个问题拆分成一个映射操作和一个简化操作,使其更容被并行化。MapReduce,使用这个术语特指一个使用多台计算机的,由映射操作和化简操作构成的,高效且容错的分布式系统,Hadoop就是一个MapReduce系统可以做到:
    • 将输入分配给多个mapper,每个mapper都会产生一些键值对;
    • 这些键值对会被发送给reducer产生最终的输出(通常也是一系列键值对)
    • 每个reducer对应的键是不同的,因此具有相同键的键值对都会发送给统一reducer进行处理
第二天 批处理层
  • 传统数据系统的缺陷:扩展性,维护成本,复杂度,人为错误,报表与分析等
  • 信息可以分为原始数据和衍生信息,原始数据是永恒的真相,而且是不变的。基于这个特性,利用Lambda架构的批处理层,可以创建具有以下特性的系统:
    • 高度并行化,可以处理TB级别的数据
    • 简单,容易创建且不易出错
    • 对技术性故障和人为故障进行容错
    • 支持日常数据的操作,也支持对历史数据生成报表和进行分析
    • 批处理层最大缺点在于有延迟,Lambda架构利用加速层来解决这一问题
第三天 加速层
  • 加速层:有新数据产生时,一方面将其添加到原始数据中,这样批处理层就可以进行处理;另一方面将其传给加速层,加速层会生成实时视图,实时视图会和批处理视图合并来满足对最新数据的查询。实时视图仅包含最后一次生成批处理视图后产生的原始数据所对应的衍生信息,当这部分数据被批处理层处理后,该实时视图将被弃用。加速层使用增量算法。
  • Hadoop主要负责批处理,Storm主要负责实时处理,其能方便地使用多台计算机进行分布式计算,以改善性能和容错性。
  • 加速层创建的实时视图包含了最后一次生成批处理视图后产生的数据,这样就完善了Lambda架构。加速层可以是同步的或异步的—-Storm是一种构建异步加速层的方法:
    • Storm实时地处理元组流,元组由spout创建,由bolt处理,由topology调度
    • spout和bolt都包含多个worker,这些worker并行执行,且分布在集群的多个节点上
    • Storm默认使用”至少会执行一次”的策略—bolt需要处理元组被重试的情况
  • Lambda架构将我们已经学过的内容融合在一起:
    • 原始数据是永恒的真相,这让我们想到Clojure分离标识和状态的做法
    • Hadoop并行化解决问题的方式是先将问题切分并映射到一个数据结构上,再进行化简操作,这非常类似于并行函数编程的做法。
    • 类似于actor模型,Lambda架构将处理过程分布到集群上,这样即改进了性能,又可以对硬件故障进行容错
    • Storm的元组流类似actor模型和CSP模型的消息机制。
  • 优点:
    • Lamdba架构主要用于解决大规模数据的问题—-这些问题是传统数据处理架构难以应对的。Lambda架构非常适合于报表和分析—-以前我们会使用数据仓库来进行这类工作
  • 缺点:
    • Lambda架构最大的优点—擅长处理大规模数据—这也正是它的缺点。除非你的数据达到TB甚至更多,否则其成本(计算成本和智力成本)将高于收益
  • 替代方案:

第9章 圆满结束

  • 未来是”不变”的,不变性在代码中应用会越来越广泛,与其关系最大的是函数式编程,在函数式编程中,避免使用可变状态会使得并行和并发更为简单
  • 未来是分布式的
  • Fork/Join模型Work-Stealing算法, Java Fork/Join框架以及Go语言的goroutine的协调机制算法
  • 数据流反应型编程,函数式反应型编程,网格计算,元组空间

总结:粗略过完,在数据不变性编程思维上有更深刻的认识和理解,在不变性为前提下对函数式编程,并行,并发粗略了解,并在以此认识到在大数据分布式中是如何解决传统的问题的,后续需要重读,为了下次阅读有更好的效果,重读前需要先做的事情 TODO 1. Clojure再好好看看 2. 多看些CSP相关的论文。

  • 20180218012622