之前看过Shuo Chen的《Linux多线程服务端编程》,越看写的越牛逼,作者从多核时代分布式并发的角度去讲自己一个网络库muduo的实现,以TCP作为IPC的主要方式。自然对分布式编程涉猎就比较多了,而自己最近闲暇时再看一本《Is Parallel Programming Hard, And, If So, What Can You Do About It?》,作者从共享内存的角度阐述了多核系统并行的设计。是大神Paul E. McKenney的作品,最新的是2014版,不过没有中文翻译,貌似2011那版有人翻译为中文叫《深入理解并行编程》,可以下载去看看。不过还是原版有味道,当然看的时候就比较蛋疼了。
今天起开始整理自己看得东西,作为笔记记录。
如果一个给定的线程基本不交互,那么他就没有作用并且不大需要被执行。但是,由于交互会引起开销,不仔细的分割选择会导致严重的性能退化。而且,并行线程的数量常常必须被控制,因为每个线程都会占用一些资源,比如 CPU cache 空间。如果过多的线程同时执行,CPU cache 将会溢出,引起过高的 cache miss,从而降低性能。从另外一方面看,大量的线程可能会带来大量计算和 I/O 操作。
[并行访问控制]
第一个并行访问控制问题是访问特定的资源是否受限于资源的位置。比如,在许多消息传递环境中,本地变量的访问是通过表达式和赋值,但是远程变量访问是通过一套完全不同的语法,经常引入消息传递机制。POSIX 线程环境,结构化查询语言(SQL),以及分割的全局地址空间环境例如通用并行 C,提供了隐式访问,然后消息传递结构通常提供显式的访问,因为访问远程数据需要显式的消息传递。
其他的并行访问控制问题是线程如何协调访问资源。这种协调是由非常多的同步机制通过提供不同的并行语言和环境来实施的,包括消息传递,加锁,事务,引用计数,显式计时,共享原子变量,以及数据所有权。需要传统的并行编程关注由此引出的死锁,活锁,和事务回滚。这种框架如果要详细讨论需要涉及到同步机制的比较,比如加锁相对于事务内存。
「资源分割」
对于资源的分割,比较明智的做法是通过分割写资源操作和读资源操作来开始并行化。这些频繁访问的数据,
可能会在计算机系统间,大存储设备间,NUMA 结点间,CPU 之间(或者核与硬件线程),页面,cache 缓存线,同步原语的实例,或者代码临界区之间造成问题。比如,分割加锁原语叫做“数据加锁”。
硬件交互通常是操作系统的核心,编译器、库,或者其他的软件环境基础,开发时需要考虑目标硬件的cache分布以及系统拓扑结构等因素。
「内存引用」
CPU 设计者仍然可以构造单周期访问的 4KB内存,即使是在几 GHz 时钟频率的系统上时钟频率的系统上。事实上这些设计者经常构造这样的事实上这些设计者经常构造这样的内存,但他们现在称呼这种内存为他们现在称呼这种内存为“0 级 cache”。
「原子操作」
原子操作的概念在某种意义上与 CPU 流水线上的一次执行一条的汇编操作冲突了的一次执行一条的汇编操作冲突了。拜硬件设计者的精密设计所赐,现代现代 CPU使用了很多非常聪明的手段让这些操作看起来是原子的
使用了很多非常聪明的手段让这些操作看起来是原子的,即使这些指令实际上不是原子的。不过即使如此,也还是有一些指令是流水线必须延迟甚至需要冲刷也还是有一些指令是流水线必须延迟甚至需要冲刷也还是有一些指令是流水线必须延迟甚至需要冲刷,以便一条原子操作成功完成。
「内存屏障」
原子操作通常只用于数据的单个元素原子操作通常只用于数据的单个元素。由于许多并行算法都需要在更新多个数据元素时,保证正确的执行顺序保证正确的执行顺序,大多数 CPU 都提供了内存屏障供了内存屏障。内存屏障也是影响性能的因素之一.为了防止这种有害的乱序执行,锁操作原语必须包含或显式或隐式的内存屏障.由于内存屏障的作用是防止 CPU 为了提升性能而进行的乱序执行为了提升性能而进行的乱序执行,所以内存屏障几乎一定会降所以内存屏障几乎一定会降低CPU 性能。
「cache miss」
现代 CPU 使用大容量的高速缓存来降低由于较低的内存访问速度带来的性能惩罚。但是,,CPU 高速缓存事实上对多 CPU 间频繁访问的变量起反效果。因为当某个 CPU 想去想去更改变量的值时,极有可能该变量的值刚被其极有可能该变量的值刚被其他 CPU 修改过。在这种情况下在这种情况下,变量存在于其他 CPU 而不是当前 CPU 的缓存中,这将导致代价高昂的 Cache Miss.
「I/O操作」
缓存未命中可以视为 CPU 之间的 I/O 操作,这应该是代价最低廉的这应该是代价最低廉的 I/O 操作之一.
共享内存式的并行计算和分布式系统式的并行计算的其中一个不同点是共享内存式并行计算的程序一般不会处理比缓存未命中更糟的情况,而分布式并行计算的程序则很可能遭遇网络通信延迟。这两种情况的延迟都可看作是通信的代价——在串行程序中所没有的代价。因此,通信的开销占执行的实际工作的比率是一项关键设计参数。并行设计的一个主要目标是尽可能的减少这一比率,以达到性能和可扩展性上的目的。
「设计」
并行算法必须将每个线程设计成尽可能独立运行的线程。越少使用线程间通信手段,比如原子操作、锁或者其它消息传递方法,应用程序的性能和可扩展性就会更好。简而言之,想要达到优秀的并行性能和可扩展性,就意味着在并行算法和实现中挣扎,小心的选择数据结构和算法,使用现有的并行软件和环境,或者将并行问题转换成已经有并行解决方案存在的问题。
「follow question」 :if we are going to have to apply distributed-programming techniques to shared-memory parallel programs, why not just always use these dis-tributed techniques and dispense with shared memory?
1. The good news is that multicore systems are inex-pensive and readily available.
2. More good news: The overhead of many synchro-nization operations is much lower than it was on parallel systems from the early 2000s.
3. The bad news is that the overhead of cache misses is still high, especially on large systems.