精华内容
下载资源
问答
  • C++多核编程+代码

    2012-08-24 13:24:22
    C++多核编程及其代码,2010年发行,是多核编程的经典书籍,需要的人就不用我说了
  • 多核计算与并发编程 语言一次我们说到,在多核或集群的环境下,可以提高系统整体的吞吐能力,这种架构的设计,和语言是无关的,但是有些语言,具有更好的适应并发环境编程的能力。我这里把编程语言分四类...

    多核计算与并发编程  语言篇

    上一次我们说到,在多核或集群的环境下,可以提高系统整体的吞吐能力,这种架构的设计,和语言是无关的,但是有些语言,具有更好的适应并发环境编程的能力。我在这里把编程语言分四类来讲述它们的差异(为什么只分四类,因为我这里是砖,要等你的玉来补充不是吗)。

    第一类,单进程解释语言 python, ruby, node.js等

    这类解释语言通常提供极高的开发效率,和相对较差的执行效率,在多核与并发的世界,它们不提供任何支持。执行一段代码时,无法使用到第二个cpu内核。所以执行这类语言开发的服务,想要利用所有的cpu,只能依靠架构和部署。

    在多核和集群的环境下,这类语言的部署是类似的,就是启动多个进程,各自独立地响应服务请求,来提升系统整体的并发吞吐能力,对外接口方面,需要硬件或者软件的负载均衡代理层。如果有进程内的可变缓存对象,开发时需要考虑数据同步。

    第二类,共享内存的多线程语言 java, .net等

    这类语言在设计之初都注重性能,具有较高的执行效率。在单核的年代,他们提供了创建操作系统线程的能力,可以在一个运行进程内,充分利用cpu的运算能力(当一个空闲线程等待时,其它的线程可以运行)。当多核cpu发明以后,那些支持多线程的软件,自动就具有了支持多核的能力。然而多核出现是在这类语言发明之后,真正的并发执行发生后,原先单核环境下不会发生的问题暴露出来,于是java和.net纷纷升级语言,提供补丁,以更好的支持多核并发的环境。

    那么,并发执行暴露出来的是什么问题。通常是因为并发执行的进程访问了共享的内存,由于读写的次序不可预料,会产生不可预料的结果。怎么解决这个问题,java和.net提供的方法是加锁,就是到一个进程(线程)访问共享内存时,不允许其它进程(线程)访问。写加锁的代码对程序员的要求很高,一不小心就会发生死锁,而一旦发生死锁,排查错误非常困难。

    在架构方面,在多核的单台服务器环境下,不需要运行多个进程也能提高吞吐能力,简化了部署。在集群环境下,和第一类语言相同。

    第三类,不共享内存的多线程语言 Erlang

    Erlang诞生已经二三十年,设计之初,是为集群设计,提供了集群与单机一致的开发方法,从语言层面,消除了单机和集群的差异。用Erlang在单机开发的软件,可以轻松扩展运行在整个集群上。这是架构层面的简化。

    Erlang是函数式语言,变量只能赋值一次,然后不可改变,同时,Erlang不允许进程间共享数据,从语言层面避免的并发编程最容易引起的错误。因为进程间不允许共享数据,所以Erlang也不需要加锁解锁的语句,我猜语言本身在底层实现用到了加锁原语,但程序员可以和繁复的锁告别,是足够幸福的。每个进程都可以安全的并发执行,但在进程内部,所有的操作都是串行的。往好处想,既享受到了多核和集群的好处,又避免了产生编程错误的可能,正是目前Erlang持续升温的原因。不足的地方是,不适合某些场景的应用,比如缓存服务,当我有一大块内存提供缓存服务时,只能有一个进程来读写这块内存,无法进一步提升服务性能。

    第四类,为并发设计的混合式语言 Golang

    在介绍Golang之前,先总结Erlang适应多核编程的特点

    1. 单个服务能利用到多核

    2. 进程间不共享内存

    3. 进程间用消息通讯,不用加锁机制

    4. 进程间可以跨服务器通讯

    之前我们说的第一类语言比如python,不能做到第一点。第二类语言java和.net,不能做到第三点,所有的语言都不能做到第四点(需要额外开发,不能在语言内部支持)。

    如果以Erlang的思想来写Golang的代码,可以做到1,2,3点。你大概能知道Golang是怎么回事了。

    Go语言的特色是,很多事情你都“可以”做。可以进程间使用消息管道来替代锁,也可以使用锁。可以使用函数式编程,也可以使用面向对象开发。可以共享内存,当然开发人员可以选择不共享内存。可以方便创建多个进程来并发执行,也可以指定最多的并发数目,限制cpu资源消耗。

    参考Erlang的设计思想,我们开发Go语言应用时,可以做到“1”和“3”,便利地使用多核来提高性能,同时降低开发门槛。在我们《架构篇》中提到的场景,网页服务的场景可以做到“2”,进程间不共享内存;而缓存服务的场景,我们可以使用多进程来访问读取共享缓存,提高吞吐能力,而同时保证只存在一个进程,来修改共享缓存,避免写入冲突。

    注意,这里有个边界状况,你需要留意,而我在这里忽略了。也就是写入操作完成一半时,可能被读取进程读到不完整的数据,在我这里的场景,并不太在乎。如果一定要保证读取数据一致,将不得不引入加锁机制,这是多么可怕。

    在下一篇,我会讲讲怎样用Erlang的思想写Go语言的代码

    展开全文
  • 多核CPU编程

    2020-04-28 11:20:53
    多核CPU编程 共享状态式并发 涉及到可...1 如何在多核CPU使程序高效运行 使用大量进程 避免副作用 不要使用共享式的ETS或DETS表 ETS表可以被个线程共享 ETSDETS原本并不是为了独立使用而创建的,而是为了实...

    多核CPU编程

    • 共享状态式并发 涉及到可变状态(可修改的内存)
    • 消息传递式并发
      • Erlang没有可变的数据结构
      • 没有可变的数据结构 == 没有锁
      • 没有可变的数据结构 == 能够轻松并行
    • 面向并发编程

    1 如何在多核CPU上使程序高效运行

    • 使用大量进程
    • 避免副作用
      • 不要使用共享式的ETS或DETS表
      • ETS表可以被多个线程共享
      • ETS和DETS原本并不是为了独立使用而创建的,而是为了实现Mnesia。原本的 意图是如果应用程序想要模拟进程间共享内存,就应该使用Mnesia的事物机制。
    • 避免顺序瓶颈
      • 指的是多个并发进程需要访问某个顺序资源。
      • 一个典型的例子是I/O。
      • 通常只有一个磁盘,对这个磁盘的所有输出最终都将是顺序的。
      • 这个磁盘只有一组磁头,而不是两组,我们无法改变这一点。
      • 每次创建注册进程时都可能形成一个顺序瓶颈,所以要尽量避免使用注册进程。
    • 让顺序代码并发
      • 一种让顺序程序加速的简单策略是用新版的map(pmap)来取代所有的map调用,它会并行执行所有的参数
    • 编写“小消息,大计算”的代码
    展开全文
  • 基于多核的并行编程

    千次阅读 2018-09-08 17:53:36
    当有个线程操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,一个时间段的线程代码运行时,其它线程处于挂...

    概述

    摩尔定律

    当价格不变时,集成电路上课容纳的晶体管数目,约每隔18个月便会增加一倍。


    第一章

    并行与并发

    • 并行(Concurrency):two or more progress are in progress at the same time. 当系统有一个以上CPU时,则线程的操作有可能非并发,当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种烦那个是我们称之为并行。
      这里写图片描述
    • 并发(Parallelism):two or more progress are executing at the same time. 当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。这种方式我们称之为并发。
      这里写图片描述
    • 联系:二者都是用来运行多个线程,提高系统响应性能与程序友好性,充分利用CPU资源的方式。
    • 区别:并发是一个处理器以“挂起->执行->挂起”的方式同时处理多个任务,而并行是多核的处理器同时处理多个任务。

    扩展性(Scalability)

    • A computer system, including all its hardware and software resources, is called scalable if it can scale up (i.e. improve its resources) to accommodate ever-increasing performance and functionality demand and/or scale down (i.e. decrease its resources) to reduce cost.
    • More specifically, saying a system is scalable implies the following:
      – Functionality and performance
      – Scaling in cost
      – Compatibility
    维度
    • Resource Scalability
      Resource scalability refers to gaining higher performance or functionality by increasing the machine size (i.e. the number of processors), investing in more storage (cache, main memory, disks), improving the software, etc.
    • Application Scalability
      To fully exploit the power of scalable parallel computers, the application programs must also be scalable. This means the same program should run with proportionally better performance on a scaled-up system.
      – e.g. A database server.
    • Technology Scalability
      Technology scalability applies to a scalable system that can adapt to changes in technology.
      – Generation (time) scalability. e.g. PC.
      – Space scalability. e.g. SMP & MPP vs. Internet
      – Heterogeneity scalability. e.g. PVM

    加速比(重点)

    • 定义:描述对程序并行化之后获得的性能收益
    • 公式:加速比(nt)=最优串行算法执行时间/并行程序执行时间(nt)
    Amdahl定律
    • 定义:固定问题规模(工作负载),减少响应时间
    • 公式:加速比 = 1/[S + (1-S)/n]
      S: 程序中串行部分的比例,n: 机器规模(或处理器核的数目)
    Gustafson定律
    • 定义:固定计算时间,扩大问题规模,提高计算精度
    • 扩展加速比=(1-S)*n + S

    第二章

    Flynn分类法

    Flynn proposed a classification of computer systems based on a number of instruction and data streams that can be processed simultaneously.

    • SISD (Single Instruction, Single Data stream)
      这里写图片描述
    • SIMD (Single Instruction, Multiple Data Streams)
      这里写图片描述
    • MISD (Multiple Instructions, Single Data Stream)
      Heterogeneous systems operate on the same data stream and must agree on the result(只有理论模型)
    • MIMD (Multiple Instructions, Multiple Data Streams)
      这里写图片描述
    Shared Memory MIMD Machine(共享存储)

    这里写图片描述

    Distributed Memory MIMD(分布式存储)

    这里写图片描述


    存储分级

    这里写图片描述


    地址空间

    地址空间(address space)表示任何一个计算机实体所占用的内存大小。比如外设、文件、服务器或者一个网络计算机;进程或线程可访问的地址空间。


    三个操作&四个开销

    三个操作
    • Computation operations
      Including arithmetic/logic, data transfer, control flow operations that can be found in a traditional sequential program.
    • Parallel operations
      To manage processes/threads, such as creation and termination, context switching, and grouping.
    • Interaction operations
      To communicate and to synchronize processes/threads.
    四个开销
    • Parallelism overhead caused by process/thread management.
    • Communication overhead caused by processors exchanging information.
    • Synchronization overhead in executing synchronization operations.
    • Load imbalance overhead incurred, when some processors are idle while the others are busy.

    不可并行

    • Certain cryptographic hash functions一些加密Hash函数
    • Newton’s method: you need each approximation in order to calculate the
      next, better approximation当下一轮的运算紧密依赖于之前的运算结果时
    • 当运算不可以被拆分为独立模块时
    举例:递推

    PRAM模型(Parallel Random Access Machine)

    规模为n的PRAM模型定义:n个处理器,1个共享空间,一个公共时钟。
    这里写图片描述

    特点
    • n可以无限大
    • 在一个周期内,每个处理器只执行一条指令
    • 只计算负载不平衡开销
    • 一条指令可以是任何随机访问指令
    • 地址空间:单地址空间、均匀存储器访问
    • 存储器模型:EREW

    APRAM模型

    特点
    • 每个处理器都有其本地存储器、局部时钟和局部程序
    • 处理器间的通信经过共享全局存储器
    • 无全局时钟,各处理器异步地独立执行各自的指令
    • 处理器任何时间依赖关系需明确地在各处理器的程序中加入同步路障
    • 一条指令可在非确定但有限的时间内完成。
    与PRAM对比
    • 每个处理器有本地存储器,处理器中的运算基于本地存储器的数据
    • 各处理器在局部时钟下异步独立地执行各自的指令
    • 需要在各处理器的程序中加入同步路障,在该点的处理器均需要等待别的处理器到达后才能继续执行其局部程序

    BSP模型

    特点
    • 克服PRAM模型的缺点,保留其简单性
    • 一个BSP计算机由n个处理器-存储器对(节点)组成,它们之间借助通信网络进行互连
    • 分布式存储的MIMD模型
    • BSP模型中,计算由一系列由同步路障分开的超步级(superstep)组成
    执行超步最大时间:

    T=W+gh+l

    • W:每个超步内的最大计算时间
    • l:路障同步开销
    • G:发送每条消息的开销
    • h:一个处理器在一个超步中最多发送消息数

    多级存储体系结构

    这里写图片描述

    映射策略
    • 直接映射策略(direct mapping strategy):每个内存块只能被唯一地映射到指定的一条cache line中
    • n路组关联映射策略(n-way set association mapping strategy):Cache被分解为V个组,每个组由n条cache line组成,内存块按直接映射策略映射到某个组,但在该组中,内存块可以被映射到任意一条cache线
    • 全关联映射策略 (full association mapping strategy):内存块可以被映射到cache中的任意一条cache line

    并行计算机访存模型

    UMA(Uniform Memory Access)模型
    • 物理存储器被所有节点共享
    • 所有节点访问任意存储单元的时间相同
    • 发生访存竞争时,仲裁策略平等对待每个节点,即每个节点机会均等
    • 各节点的CPU可带有局部私有高速缓存
    • 外围I/O设备也可以共享,且每个节点有平等的访问权利
    NUMA(Non-Uniform Memory Access)模型
    • 物理存储器被所有节点共享,任意节点可以直接访问任意内存模块
    • 节点访问内存模块的速度不同,访问本地存储模块的速度一般是访问其他节点内存模块的3倍以上
    • 发生访存竞争时,仲裁策略对节点可能是不等价的
    • 各节点的CPU可带有局部私有高速缓存 (cache)
    • 外围I/O设备也可以共享,但对各节点是不等价的

    CC-NUMA的协议

    • 写无效协议:在本地高速缓存被修改后,使得所有其它位置的数据拷贝失效
    • 写更新协议:在本地高速缓存被修改时,广播修改的数据,使得其它位置的数据拷贝得以及时更新

    内存访问模型分类

    这里写图片描述


    SMP(Symmetric Multi-Processor)

    这里写图片描述

    优点
    • 结构对称,采用单一操作系统
    • 所有处理器通过高速总线或交叉开关与共享存储器相连,具有单一的地址空间
    • 通过写/读共享变量完成通信,快捷且编程比较容易
    缺点
    • 存储器和I/O负载大,易称为系统瓶颈,限制了系统中处理器的数量
    • 单点实效就会导致整个系统的崩溃
    • 一次成型,扩展性差

    PVP (Parallel Vector Processors)

    这些系统含有为数不多,功能很强的定制向量处理器(单个处理器性能至少为1Gflop/s),它们通常不使用高速缓存,而是采用大量向量处理器和指令缓存


    MPP与Cluster的区别

    MPP

    这里写图片描述

    Cluster

    这里写图片描述

    对比
    • MPP使用定制网络,而Cluster使用价格便宜的商用网络
    • MPP的网络接口是连接到节点的存储总线上(紧耦合),而Cluster的网络接口是连接到I/O总线上(松耦合)
    • Cluster每一个节点都是完整的计算机

    第三章

    三种并行编程模型

    特点

    这里写图片描述

    区别

    这里写图片描述


    显式并行与隐式并行

    显式并行
    • 在源程序中由程序员使用专用语言构造、编译器命令或库函数对并行性加以显式说明
    • 共享变量模型、消息传递模型、数据并行模型
    隐式并行
    • 程序员不显式地说明并行性,而是让编译器或运行支持系统自动加以开发
    • 并行化编译器、运行时间并行化

    并行程序设计模型

    • 任务并行模式(任务分解或数据分解)
    • 分治模式(任务分解或数据分解):如快排算法
    • 几何分解(数据分解):将所要解决问题中使用的数据结构并行化,每个线程只负责一些数据块上的操作
    • 流水线模式(数据流分解)

    第四章

    进程和线程

    • 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等,但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器上下文(register context),自己的线程本地存储(thread-local storage)。
    • 进程:是一个可并发执行的具有独立功能的程序关于某个数据集的一次执行过程,也是操作系统进行资源分配和保护的基本单位
    • 线程支持/实现方法:
      在单核CPU机器上,操作系统以循环的方式为依次每个独立线程提供量子(quantum),得到quantum的线程开始执行;
      可以通过操作系统层(如Win32 API)实现;
      可以通过库或运行时环境(MFC,.net框架)实现;
      可以通过专门的多线程库(pthread)实现。

    互锁函数

    • 以原子操作的方式修改一个值
    • 相比其他同步(互斥)方式,速度极快
    特点
    • 许多现代计算机体系结构都会支持一些特殊指令,这些指令可以快速执行普通的原子操作,而不需要获取同步对象。Windows通过互锁函数利用这样的特性。
    • 对x86家族的CPU来说,互锁函数会对总线发出一个硬件信号,防止另一个CPU访问同一内存地址

    Spin Lock(自旋锁/循环锁)

    非阻塞锁。由某个线程独占。采用循环锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到获得该锁。

    应用场景
    • 锁持有时间较短
    • 避免在单CPU或单核计算机中使用循环锁

    用户态同步和内核态同步

    用户态同步

    允许线程保留在用户方式下实现同步。如互锁函数,CRITICAL_SECTION

    • 优点:速度快
    • 局限:互锁函数只能在单值上运行;CRITICAL_SECTION只能对单个进程中的线程同步
    内核态同步

    使用内核对象进行同步

    • 优点:适应性广泛
    • 缺点:速度慢
    可用于同步的内核对象
    • Processes, Threads, Jobs, Files
    • Events
    • Semaphores, Mutexes
    • File change notifications
    • Waitable timers
    • Console input

    For thread synchronization, each of these kernel objects is said to be in a signaled or nonsignaled state.

    操作
    • Wait Functions(原子操作)
    • Event Kernel Objects
    • 信号量对象
    • 互斥量对象
    • 线程池
    • 线程优先级
    • 处理器亲和

    线程局部存储(Thread Local Storage)

    – 线程局部存储(thread-local storage,TLS)是一个很方便的存储线程局部数据的系统
    – 可以使用TLS将数据与一个特定的线程相关联
    – 利用TLS机制可以为进程中的所有线程关联若干个数据,各个线程通过TLS分配的全局索引来访问自己关联的数据

    用法
    • TlsAlloc函数:系统为每一个进程都维护一个长度为TLS_MINIMUM_AVAILABLE的位数组,TlsAlloc的返回值就是数组中值为FREE的一个成员的下标(索引),如果找不到一个值为FREE的成员,TlsAlloc返回TLS_OUT_OF_INDEXES,意味着失败
    • TlsSetValue函数:该函数将lpTlsValue参数所确定的值放进线程的数组中,而放置位置的索引则是由dwTlsIndex确定的lpTlsValue的值关联到调用TlsSetValue的线程,如果调用成功则返回TRUE; 当一个线程调用TlsSetValue时,可以改变其自身的
      数组,但是它不能为另外的线程设置TLS的值
    • TlsGetValue函数
    • TlsFree函数

    第五章

    exec和fork

    • exec:系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号以及一些环境变量的信息;
    • fork:启动一个新的进程,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了

    Signal(信号量)

    这里写图片描述
    这里写图片描述


    不可靠信号

    在早期的UNIX中信号是不可靠的,不可靠在这里指的是:信号可能丢失,一个信号发生了,但进程却可能一直不知道这一点。
    现在Linux在SIGRTMIN实时信号之前的都叫不可靠信号,这里的不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。
    可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的队列。等进程有机会来处理的时候,依次再处理,信号不丢失。


    可重入函数

    可以被中断的函数

    不可重入函数
    • 系统资源
    • 全局变量
    • 使用静态数据结构
    • 调用malloc或者free
    • 标准IO函数
    • 例子:getpwname(),errno,reenter.c

    共享内存

    • 共享内存是内核为进程创建的一个特殊内存段,它可连接(attach)到自己的地址空间,也可以连接到其它进程的地址空间
    • 最快的进程间通信方式
    • 不提供任何同步功能
    展开全文
  • 最近重读《the art of ...另外,个人感觉英语作为一门层次化的结构立体性的语言,其突出重点,并随时可以自然的为其中的任何概念增加注释(从句)的特性使其更加适合技术性文章。这也是为什么很时候看英文

    最近重读《the art of multiprocessor programming》,从初读时的不知所云,到二读时的不以为然,再到再读时字字珠玑深以为然。由此深知遇到一本书,不是你看见它的时候,也不是你买了它的时候,甚至都不是你读过它的时候,而是你深刻的与它产生了共鸣的时候。于是决定有精力的话会将其逐篇的翻译出来,以供自己进一步的加深理解。另外,个人感觉英语作为一门层次化的结构立体性的语言,其突出重点,并随时可以自然的为其中的任何概念增加注释(从句)的特性使其更加适合技术性文章。这也是为什么很多时候看英文的注释或文献会感觉讲的很清晰,而看中文的介绍和说明反而会觉得讲不清楚。中文线性化及藏头露尾的语言特性使其表达非常具体的技术细节时总觉得有些捉襟见肘。在此先尝试着将说明我们为什么需要并发编程相关知识的背景--多核系统硬件基础(附录B)翻译如下

    ------------------------------------------------------------------------------------------------------

    一个菜鸟学生正在试着关掉电源再打开来修复一个死机的电脑。Knight发现了这个学生正在干的事并厉声说道:“你不能不了解哪里出错就仅靠重启来修复一个计算机。”然后Knight将机器关掉再启动,于是它就工作正常了。(摘自"AI Koans",一个在80年代广泛流传在麻省理工学院的笑话集)

    B.1 简介(以及一个疑惑)

    你无法在不了解多处理器是什么的情况下编写一个有效的多线程程序。在不了解计算机体系结构的前提下可以很好的编写单线程程序,但是对于多线程程序而言情况就有所不同了。我们将会通过一个令人疑惑的场景来展示这一点。我们将会考虑两个在逻辑功能上相同的程序,只不过一个没有另一个高效。令人不爽的是,更简单的那个程序却是更低效的。如果不了解现代计算机的多处理器架构,则既无法解释上述不合常理,也不能避免这样的危险。

    如下是该疑惑的背景说明。假设两个线程共享一个资源,该资源每次只能被一个线程使用。为了防止同时使用,每一个线程必须在使用该资源之前锁定它,并在使用之后为其解锁。我们将在第7章学习很多实现锁的方式。对于本次的场景来说,我们考虑两个简单的实现,在这两个实现中都会使用一个单独的Boolean属性来代表锁。如果该属性值为false,则说明未上锁,否则说明已上锁。我们通过getAndSet(v)方法来操作这把锁,getAndSet(v)方法会原子性的将参数中的值设置到属性中并返回属性中原来的值。为了得到锁,线程会调用getAndSet(true)。如果返回的值为false,则说明原来锁是空闲的,调用者就成功的获取了该锁。否则,若返回值为true说明该对象早已经被上锁了,本线程必须稍后再次尝试。一个线程简单的通过将对应属性设置为false就可以释放锁了(无需getAndSet操作)。

    在代码B.1中,test-and-set锁(TASLock)会重复地调用getAndSet(true),直到该方法返回false为止。相比之下,在代码B.2中test-and-test-and-set锁(TTASLock)重复地读取表示锁的属性值(通过调用state.get(),在第5行)直到该值变为false,只有到这个时候才会调用getAndSet()(第6行)。了解下面的情况是很重要的--读取锁属性的值是原子性的,应用getAndSet()也是原子性的,但是两者组合起来不是原子性的:在一个线程读取锁属性值与调用getAndSet()方法中间,锁属性可能会被其他线程修改。

    public class TASLock implements Lock {
        ...
        public void lock() {
            while(state.getAndSet(true)) {} //spin
        }
        ...
    }
    
    Figure B.1 TASLock类
    public class TTASLock implements Lock {
        ...
        public void lock () {
            while (true) {
                while (state.get()) {} //spin
                if (!state.getAndSet(true))
                    return;
            }
        }
        ...
    }
    
    Fiture B.2 TTASLock类

    在继续前进之前,你应该说服自己TASLock和TTASLock算法在逻辑功能上是相同的。原因很简单:在TTASLock算法中,读取到锁处于可用状态并不能保证紧接着的getAndSet()调用一定能成功,因为可能有其他线程在这个间隙捷足先登得到了锁。所以为什么要搞这么麻烦先读取锁的状态再尝试获取它呢?

    这里就是令人疑惑的现象。虽然两个锁实现在逻辑功能上是等价的,但是他们的性能表现非常不同。在1989年的一个经典的实验中,Anderson在几个当时的多核处理器上测量了一个简单测试程序的执行时间。他测量了n个线程并发执行一个短暂的临界区各一百万次所消耗的时间。图表B.3展示了每个锁实现消耗的时间,在图上被描绘成一个使用线程数的函数。在一个完美的世界里,TASLock和TTASLock的曲线都应该和在底部的理想化曲线一样平坦,因为每一个线程的run方法都会执行相同数目的一百万次工作量。然而,我们看到两者的曲线都向上倾斜了,说明当线程数增加时,由锁引入的延迟也在增加。然而奇怪的是,TASLock比TTASLock的性能要慢很多,特别是当线程数增加的时候。这是为什么呢?

    本章节覆盖了很多你想要编写高效的并发算法及数据结构必须知道的多处理器体系结构知识。(顺带着,我们将会解释在图表B.3中性能曲线所表现出的差异)

    我们将会涉及到如下的组件:

    • 处理器是执行线程的硬件设备。通常会有比处理器个数更多的线程,每个处理器会执行一个线程一小会儿,然后将其放在一边,转头去执行其他线程。
    • 互联设备是一个通信中介,该通信中介连接不同的处理器,以及处理器与存储器。
    • 存储器实际上是一个保存数据的层次结构的组件,涉及到一层或多层很小但是很快的缓存,以及一个很大但是相对较慢的主内存。了解这些不同层次的存储设备是如何交互的,对于理解很多并发算法的实际性能表现是至关重要的。

    从我们的角度来看,一个架构上的原则驱动了所有的事情:处理器和主内存离得很远。处理器需要花费很长时间从主内存中读取一个值。处理器同样要花费很长时间来将一个值写到内存中,还要花更长的时间来确保该值确实被装载到内存中了。访问内存更像是邮寄一封信而不是打一个电话。我们本章考察的几乎所有东西都是为了尝试减轻访问内存的高延迟而产生的。

    处理器和内存的速度都随着时间的推移而变化,但是它们之间的相对性能变化比较缓慢。让我们考虑一下如下的类比。想象一下现在是1980年,你负责曼哈顿市中心的送信业务。虽然在开阔的路上汽车的性能好过自行车,但是在交通拥堵的地方自行车的表现更好,所以你选择使用自行车。尽管自行车和汽车背后的技术都在不断改进,但是它们在架构上的对比仍然是相似的。所以直到现在,如果你设计一个处于都市中的送信业务,你仍然应该选择使用自行车,而不是汽车。

    B.2 处理器和线程

    一个多处理器系统包含了多个硬件上的处理器,每一个都可以执行一个顺序的程序。当讨论多处理器架构时候,基本的时间单元是指令周期:一个处理器取出并执行一条指令所花费的时间。按绝对值计算,随着技术的进步指令周期也在变化(从1980年的每秒1千万次到2005年的大约每秒30亿次),并在在不同的平台上也会有所差异(控制烤箱的处理器与控制网站服务器的处理器相比,指令周期要长的多)。然而,访问内存所需耗费的指令周期数的变化非常缓慢。

    一个线程是一个顺序的程序。相较于处理器是一个硬件设备,线程则是一个软件结构。一个处理器可以运行一个线程一小会,然后将其放在一边并去运行另外一个线程,该事件被称为上下文切换。处理器有很多原因会将线程放在一边,或者取消调度它。或许这个线程下达了一个内存请求,该请求需要等待一些时间才能满足;又或者仅仅是因为该线程已经运行了足够久,是时候让另一个线程执行一会儿了。当一个线程被取消调度,它可能会被另一个处理器调度继续执行。

    B.3 互联设备

    互联设备是处理器用来和存储器及其他处理器通信的中介。实质上当前在使用的有两种类型的互联架构:SMP(对称多核处理器架构)以及NUMA(非统一内存访问架构)。

    在一个SMP架构中,处理器和内存会通过一个总线连接起来,该总线是一个看起来类似于以太网的广播媒介。处理器和主内存都有总线控制单元,负责发送和监听来自总线广播的消息(有时也称为嗅探)。在今天,SMP架构是最常见的架构,因为它们最容易构建,但是受限于总线过载而无法扩展到较大的处理器个数。

    在一个NUMA架构中,一群节点通过点对点的网络连接在一起,像一个小的局域网。每个节点包括一个或多个处理器,以及一个本地的内存。一个节点的本地内存也可以被其他节点访问,所有节点的内存共同构成一个所有处理器共享的全局内存。NUMA的命名就说明了一个处理器访问自己所在节点内存的速度快过其他节点中的内存。网络比总线更复杂一些,要求更加复杂的协议,但是它们在扩展性上比总线要好,可以扩展到很大数目的多处理器。

    SMP和NUMA架构的划分有一点简单化:当然也可以设计混合架构,在一个集群内的处理器通过总线通信,但是不同集群之间的处理器通过网络通信。

    从程序员的视角来看,底层平台是基于总线,网络,还是一个混合的互联结构看起来似乎没有那么重要。然而,意识到互联设备是一个所有处理器共享的有限资源,这点是非常重要的。如果一个处理器占用了过多的互联设备的带宽,那么其他处理器就会被延误了。

    B.4 存储器

    多个处理器共享一个主内存,该主内存是一个使用内存地址进行索引的巨大的字数组。与平台有关,一个字一般是32或64位,内存地址也一样。稍微简化一下,处理器通过发送一个包含了想要地址的消息给内存,以从内存中读取一个值。响应消息包含了对应的数据,也就是内存中对应地址中的内容。处理器通过向内存发送地址和新的数据来将数据写入内存,当新数据被装载之后,内存会发送回一个通知。

    B.5 缓存

    不幸的是,在现代的架构中,一个主内存的访问可能会消耗掉数百个指令周期,因此会存在这样的危险,处理器可能浪费掉大部分时间仅仅是在等待内存的响应。我们可以通过引入一层或多层缓存来缓解这个问题。缓存是小的存储设备,其位置更加靠近处理器,因此处理器访问其速度会远远快过主内存。

    这些缓存在逻辑上的位置处于处理器与主内存之间:当一个处理器想要读取给定地址的数据时,它首先查看缓存中是否已经有该数据了,若有,它就不需要执行缓慢的内存访问了。如果在缓存中找到了想要地址的值,我们称为缓存匹配(cache hit),否则称为缓存缺失(cache miss)。同样的方式,如果一个处理器想要写一个已在缓存中的地址,它也不需要执行缓慢的内存访问。请求被缓存满足的比例称为缓存匹配率。

    由于绝大部分程序都表现出一种高度的局部性,因此缓存是很有效的。局部性是指如果一个处理器读或写了某一个内存地址(也称为一个内存位置),那么它很可能很快会再次读或写相同的位置。而且,如果一个处理器读或写某一个内存位置,那么它很可能也会很快读或写相邻的内存位置。为了利用后一个观测结果,缓存往往会操作比单个字宽更大的粒度:缓存会持有被称为缓存线(cache lines,有时候也被称为缓存块cache blocks)的一组位置相邻的字宽数据。

    实践中,大多数处理器都有两层缓存,称为L1和L2缓存。L1缓存通常与处理器集成于同一块芯片上,其访问会消耗1到2个指令周期。L2缓存可能会被集成到芯片中,也可能不会,访问L2缓存可能会消耗数十个指令周期。两者都远远快过访问主内存的数百个指令周期。当然,这些时间在不同的平台上会有不同,并且一些多处理器有更加复杂的缓存架构。

    NUMA架构最初的白皮书中并没有包含缓存,因为觉得本地内存就已经够用了。然后,后来商用的NUMA架构中确实引入了缓存。有时候术语cc-NUMA(缓存一致性NUMA)用来表示带缓存的NUMA架构。这里,为了避免模棱两可,我们讲的NUMA包含了缓存,除非我们另有说明。

    缓存的构建非常昂贵,因此其会远远小于内存:只有内存地址的很小一部分可以同时存在于缓存中。因此我们希望在缓存中维护最常用到的地址中的值。这意味着当我们往一个已满的缓存中再次加载数据时,就需要驱逐一个缓存线了,如果它没有被修改过就直接丢弃,否则需要将其写回主内存中。替换策略决定了由哪个缓存线来给新地址中的数据腾地方。如果替换策略可以自由的替换掉任何一个缓存线,则我们称该缓存为完全相连的(fully associative)。另一方面,如果只有一个缓存线可以被替换,那么我们称该缓存为直接映射的(direct mapped)。如果我们做个折中,对于一个给定的缓存线,允许一个大小为z的集合中的任意一条被替换,那么我们称该缓存为k路组相连的(k-way set associative)。

    B.5.1 一致性

    共享(或者不太礼貌的说,内存竞争),发生在当一个处理器读或者写一个已经被其他处理器缓存过的内存地址时。如果每个处理器不修改只是读取数据,那么数据可以同时被各处理器分别缓存。然而,如果一个处理器尝试更新共享状态的缓存线,那么其他处理器的副本必须被无效化以确保它不会读取到一个过时的值。

    一般来讲,这个问题被称为缓存一致性。参考文献中包含了各种各样复杂而聪明的缓存一致性协议。这里我们回顾一个最常用的协议,基于缓存线可能处于的状态而被叫做MESI协议。该协议已经被用在奔腾和PowerPC处理器中。下列是缓存线的状态。

    • Modified: 该缓存线已经被修改了,它最终必须被写回到主内存中。没有其他处理器持有该数据的缓存。
    • Exclusive: 该缓存线没有被修改,也没有其他处理器持有该数据的缓存。
    • Shared: 该缓存线没有被修改,其他处理器可能也持有了该数据的缓存。
    • Invalid: 该缓存线无效,没有包含有意义的数据。

    我们通过图表B.5描述的简短例子来展示该协议。为了简单起见,我们假设处理器和内存是通过总线连接的。

    处理器A从内存地址a中读取数据,并将该数据以exclusive状态加载到它的缓存中。当处理器B尝试从相同地址读取数据时,A检测到了地址冲突,并使用该地址相关联的数据响应B。现在地址a被处理器A和B以shared状态同时缓存了。如果B往shared状态的地址a(缓存线)中写入数据,它会将该缓存线状态转换为modified,然后广播一个消息给A(以及任何可能缓存了地址a中数据的处理器),让这些处理器将自己对应的缓存线状态置为invalid。如果A随后又要从地址a中读取数据,它就会通过总线广播该请求,此时B就会将修改后的数据同时发送给A和主内存以响应该请求,此时A和B缓存中的该数据所在的缓存线都会处于shared状态了。

    伪共享就是当处理器访问逻辑上独立的数据时却发生了冲突,因为它们访问的内存位置处于相同的缓存线中。这种现象展示了一种艰难的权衡:大的缓存线对于局部性原理来说是好的,但是它们增加了伪共享的可能性。可以通过确保那些可能被独立线程并发访问的数据结构在内存中彼此远离来减少发生伪共享的可能性。例如,多个线程共享一个byte数组很可能会引入伪共享问题,但是让它们共享一个双精度的整数数组就没有那么危险了。

    B.5.2 自旋

    如果一个处理器重复的测试内存中的某些内容,那么它处于自旋中,等待其他处理器改变该内容。基于硬件架构,自旋可以对整个系统的性能造成巨大的影响。

    对于一个无缓存的SMP架构而言,自旋是一个非常糟糕的主意。每次处理器读取内存,它都会消耗总线带宽而没有完成任何有用的工作。由于总线是一个广播媒介,这些发送往内存的请求可能会妨碍其他处理器的进展。

    对于一个无缓存的NUMA架构而言,假如持续测试的地址处于处理器的本地内存中,那么自旋或许是可以接受的。虽然无缓存的多处理器架构是很稀少的,但我们在考虑一个涉及到自旋的同步协议时仍然需要问一下,该协议是否允许每一个处理器自旋在它们自己的本地内存中。

    在一个带缓存的SMP或NUMA架构中,自旋会消耗极少的资源。处理器第一次读取内存地址时,它遇到一个缓存缺失(cache miss),并将该地址的内容加载到缓存线中。从此以后,只要该数据没有被修改,处理器就只是简单的从自己的缓存中重读该数据,不消耗任何互联设备带宽,该过程被称为本地自旋。当缓存状态变化了,处理器会遇到一个缓存缺失,然后观察到数据改变了,于是就停止自旋。

    B.6 缓存敏感的编程,或疑惑的破解

    我们现在知道的多到足以解释为什么在章节B.1中验证的TTASLock性能胜过TASLock了。每一次TASLock对锁属性应用getAndSet(true),它都会通过互联设备广播一个消息,而这会导致大量的通信。在一个SMP架构中,其引发的通信量有可能足够使得互联设备饱和,因此而延误所有的线程,包括正在尝试释放该锁的线程,甚至是完全没有在竞争该锁的线程。与此相对的,当锁被使用的时候,TTASLock自旋会一直读取一个锁属性的本地缓存副本,没有制造任何的互联设备上的通信,这就解释了它的更好的性能表现。

    然而,TTASLock自己也远远没有到达理想的状态。当锁被释放的时候,所有处理器中的锁属性的缓存副本都会被无效化,然后所有等待的线程都会调用getAndSet(true)方法争抢锁,这会导致一次通信量的爆发,这个通信量虽然比TASLock造成的要小,但是仍然很大。

    我们会在第7章深入讨论缓存与锁的相互作用。与此同时,这里有一些简单的方法来构建数据以避免伪共享问题。相较于Java而言,下面列出的这些技术中的一部分在提供了对内存更加细粒度控制的编程语言如C或者C++上面更容易实施。

    • 会被单独访问的对象或属性应该被对齐和填充,这样一来它们就能处于不同的缓存线中。
    • 将只读数据与经常修改的数据分开存放。例如,考虑一个其结构不会变化的链表,但是其对象的value属性经常变化。为了确保修改value属性值不会减慢链表的遍历行为,我们可以对齐并填充value属性,这样的话每一个都可以填满一个缓存线。
    • 如果可能的话,将一个对象分裂成线程本地化的片段(也就是由每个线程维护自己的那个片段)。例如,一个用于统计的计数器可以被分裂成一个计数器数组,其中每一个都专属于一个线程,每一个都存在于一个不同的缓存线中。一个共享的计数器可能会导致缓存失效时巨大的通信量,而分裂的计数器允许每个线程在不引起一致性通信的情况下更新自己的那部分片段。
    • 如果一个锁被用于保护经常被修改的数据,那么将该锁和该数据保持在不同的缓存线中,如此一来尝试获取锁的线程就不会􏰀干扰到持有锁的线程对数据的访问。
    • 如果一个锁被用于保护经常处于无竞争状态的数据,那么尝试将该锁与该数据保持在相同的缓存线中,如此一来获取锁时也会加载一些被该锁保护的数据到缓存中。

    B.7 多核和多线程架构

    在一个多核架构中,如在图表B.6中所示,多个处理器被集成在同一块芯片上。该芯片上的每一个处理器通常都有自己的L1缓存,但是它们共享一个通用的L2缓存。同一块芯片上的存储器可以通过共享的L2缓存高效的通信,避免了需要通过内存交换数据,以及引发笨重的缓存一致性协议。

    在一个多线程的架构中,一个单独的处理器可能一次执行两个或更多的线程。很多现代的处理器实际上都具备内在的并行性。它们可以以乱序的方式执行指令,或者以并行的方式(例如,同时使固定的及浮点的计算单元保持繁忙),或者甚至会在分支之前或数据被计算出之前推测性的提前执行指令(分支预测)。为了保持硬件单元繁忙,多线程处理器可以将多个程序流上的指令混合执行。

    现代的处理器架构结合了多核与多线程,多个支持多线程的内核可能会集成于同一个芯片上。在某些多核芯片上执行上下文切换的代价非常小,并以一种非常小的粒度进行,实际上达到了单条指令的粒度。这样的话,对多线程的支持就可以掩盖内存访问的高延迟:当一个线程访问内存时,处理器就立刻允许另一个线程执行,而无上下文切换的开销。

    B.7.1 宽松的内存一致性

    当一个处理器向内存中写一个值时,该值被被保存在缓存中并标记为“脏的”,意味着它必须最终被写回到主内存。在绝大部分现代处理器中,当下达写请求时,它们不会马上被应用到内存。实际上,这些写请求会被收集在一个硬件的队列中,叫做写缓冲区(或者存储缓冲区),并在稍后被一起应用到内存中。写缓冲区可以提供两个好处。首先,一次性应用多个请求总是会更有效率,这被称为批处理。第二,如果一个线程多次往同一个地址写数据,那么前面的写请求就可以被丢弃,省下了一次内存操作,这被称为写吸收。

    使用写缓冲区会导致一个非常重要的结果:读和写被送达内存的顺序并不一定是按照它们在程序中发生的顺序。例如,回顾一下第一章介绍的flag规则,其对于实现互斥的正确性是至关重要的:如果两个处理器各自首先写它们自己的flag,然后读取对方的flag,那么它们中间的一个必然会看到对方新写入的flag值。在使用写缓冲区时这一点就不再为真了,两个都有可能将写请求放入自己的写缓冲区中,但是两个写缓冲区可能都在各自处理器读取对方内存中的flag之后才被写入。这样一来,没有一个能读取到对方新写入的flag。

    编译器可能会使问题更严重。它们非常擅长于优化程序在单处理器架构中的性能。通常,这种优化需要将一个线程对内存的读和写的顺序重排。对于单线程程序这样的重排是透明的,但是在线程可以观察到写操作发生顺序的多线程程序中,重排可能会造成不可预期的后果。例如,如果一个线程往一个缓冲区中写满了数据,然后设置一个缓冲区已满的标志,而并发执行的线程可能会在看见缓存中的新数据之前先看见这个标志,导致它们读取到脏数据。第三章中描述的不正确的双重检查的锁模式(double-check locking)就是一个由Java内存模型的某些非直觉特性导致陷阱的例子。

    不同的架构提供了对于在多大程度上允许读写指令重排的不同保证。通常来讲,最好不要依赖于这些保证,并使用在接下来章节中描述的更加昂贵的技术,来阻止这种重排。所有架构都允许你强制要求你的写操作按照它们被下达的顺序执行。一个内存屏障指令(有时候也叫内存栅栏)冲刷写缓冲区,确保在本屏障之前下达的写操作对于下达该屏障指令的处理器统统可见。内存屏障经常会由原子性的如getAndSet()这样的读-改-写操作,或者由标准的并发库来透明的插入。如此一来,处理器只需要在临界区以外对共享变量执行读写指令时才需要明确的使用内存屏障。

    一方面,内存屏障的代价是昂贵的(消耗数百或更多的指令周期),因此应该只在必要的时候使用。另一方面,同步导致的错误可能非常难以追踪,因此应该更慷慨的使用内存屏障,总好过依赖复杂的且与平台有关的对指令重排限制的保证。

    Java语言自身是允许将发生在同步方法或代码块之外的对象属性的读写操作重排的。另外,Java又提供了一个关键字volatile,其可以确保发生在同步代码块或方法之外的volatile对象的读写操作不会被重排。使用这个关键字代价可能很昂贵,因此只有必要的时候才应该使用。我们注意到,原则上说,使用volatile属性可以使得前面所说的双重检查的锁算法变得没问题,但这并没有多大意义,因为访问volatile变量总是会要求同步(这就抵消了我们使用双重检查锁算法的初衷:一旦对象创建完毕,后续的调用就不再需要同步了)。

    我们对于多处理器硬件的初级读本已接近尾声了。我们会在介绍具体的数据结构及算法时继续讨论这些架构思想。此时一个模式浮现出来:多处理器程序的性能高度依赖于与底层硬件的协同作用。

    B.8 硬件同步指令

    如同在第5章中讨论的,一个现代的多处理器架构必须支持通用的功能强大的同步基元(synchronization primitives),换言之,提供一个通用图灵机的并发计算等价物。因此Java语言在实现同步时依赖于这样专门的硬件指令(也称硬件基元)就不足为奇了,这些被Java实现的同步涵盖了自旋锁和监控器(monitors)一直到最复杂的无锁化的lock-free结构。

    􏰀现代架构通常提供一到两种通用的同步基元。AMD,Intel和Sun的架构都支持compare-and-swap(CAS)指令。该指令接收3个参数:一个内存中的地址a,一个预期的值e,以及一个要更新的值v。它返回一个Boolean。它会原子性的执行如下的步骤:

    • 如果内存中的地址a􏰀包含预期的值e,
    • 那么将要更新的值v写入地址a中并返回true,􏰀
    • 否则不修改内存a,并返回false。

    在Intel和AMD的架构中,CAS被称为CMPXCHG,而在SPARC tm中其被称为CAS。Java的java.util.concurrent.atomic库提供了原子性的Boolean, integer以及reference类,这些类都通过一个compareAndSet()方法来实现CAS(由于我们的例子绝大部分使用Java所写,因此在本书其他地方都是使用compareAndSet()来指代CAS)。C#通过方法Interlocked.CompareExchange来提供相同的功能。

    CAS指令会导致一个陷阱。或许最常见的对CAS的使用场景如下。一个应用从一个指定的内存地址中读取数据a,并为该内存地址计算出一个新的值c。它想要保存c,但是只有在该地址中的数据被读取之后没有变更的前提下才可以。有人可能认为使用预期值a和更新值c来调用一个CAS操作就能完成这个目标。但有一个问题:一个线程可能已经使用另一个值b覆盖了该值,然后又一次将值a写入了该地址中。如此一来,上述compare-and-swap操作就可以成功的将值a替换为值c,但是应用程序可能并不想这么做(例如,如果该地址存储了一个指针,那么再次写入的值a有可能就是一个被回收又再次利用的对象的地址)。CAS操作成功的校验值并做了替换,但是应用程序并非做了它想做的事。这个问题被称为ABA问题,在第10章中会详细讨论。

    另一个硬件同步基元是一对指令:load-linked和store-conditional(LL/SC)。LL指令负责从一个地址a中读取数据。随后的SC指令尝试将一个新值存储到该地址中。如果自从该线程对地址a下达了先前的LL指令以来,地址a的内容没有被更改过,那么随后的SC指令就可以执行成功。如果在此间隙内地址a中的内容有变化,那么SC指令就会执行失败。

    有几个架构是支持LL和SC指令的:Alpha AXP (ldl l/stl c),IBM PowerPC (lwarx/stwcx) MIPS ll/sc,以及ARM (ldrex/strex)。LL/SC不会遭遇ABA问题,但是实践中一个线程在LL和对应的SC指令之间可以做什么是有严格限制的。上下文切换,另一个LL,或者另一个load或store指令都有可能导致随后的SC执行失败。

    节俭的使用原子性的字段及其相关方法是一个好主意,因为它们往往都基于CAS或LL/SC指令。完成一个CAS或LL/SC指令比完成一个load或store指令要耗费多的多的指令周期:它包含一个内存屏障并阻止乱序执行及各种各样的编译器优化。确切的开销依赖于很多因素,不仅在不同的平台上会有所不同,相同平台上的不同应用之间也会有所不同。只能说CAS或LL/SC可以比简单的load或store慢的多的多。

    B.9 本章说明

    John Hennessy和David Patterson给出了一个计算机体系结构的综述。英特尔的奔腾处理器使用了MESI缓存一致性协议。对于缓存敏感的编程技巧改编自Benjamin Gamsa,Orran Krieger,Eric Parsons,和Michael Stumm。Sarita Adve和Karosh Gharachorloo提供了对内存一致性模型的完美调研。

     

    ------------------------------------------------------------------------------------------------------

    翻译自《the art of multiprocessor programming》

    展开全文
  • 高并发编程知识体系

    2019-06-26 15:48:25
    来自:编程原理,作者:林振华 1.问题 什么是线程的交互方式? 如何区分线程的同步/异步,阻塞/非阻塞? 什么是线程安全,如何做到线程安全? 如何区分并发模型? 何谓响应式编程? 操作系统如何调度线程?...
  • C++线程并发(五)---原子操作与无锁编程

    千次阅读 多人点赞 2019-05-12 13:21:23
    前面介绍了线程间是通过互斥锁与条件变量保证共享数据的同步的,互斥锁主要是针对过程加锁实现对共享资源的排他性访问。很时候,对共享资源的访问主要是对某一数据结构的读写操作,如果数据结构本身就带有...
  • 网络编程和并发

    千次阅读 2018-12-25 22:51:09
    网络编程和并发 简述 OSI 七层协议。 应用层(Application)、表示层(presentation)、会话层(session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。 每一层实现各自的...
  • vc++网络编程 线程As modern programs continue to get more complex in terms of both input and execution workloads, computers are designed with more CPU cores to match. To achieve high performance for ...
  • 本书重点讲述:共享存储器通信方式下的多处理器编程技术,这样的系统称为共享存储器的处理器,现在也称之为多核。 本书原理部分着重于可计算性理论:理解异步并发环境中的可计算问题。理解可计算性的关键在于...
  • 并发编程笔记

    2021-05-30 00:03:50
    操作系统在运行一个程序的时候,会创建一个进程,这个进程里可以创建个线程,CPU给每个线程分配时间片进行调度执行。 CPU通过时间片分配算法循环执行任务,当前任务执行一个时间片后切换到下一个任务,每次...
  • 本篇Blog为接下来的Java并发编程精华版本,重点知识,如果某个知识点不理解,可以再深入的看本专栏中的其它Blog内容介绍。为了突出重点,本篇Blog以问答的形式展示,将所有的知识点进行串联。 ...
  • 各种类型的编程语言在解决特定领域问题具有独有的编程模型,例如异步模型和多线程模型,语言最初设计者考虑哪种模型至关重要 目的:不管哪种模型,其目的是为了程序的运行可持续性,也就是任务执行解决方案。不...
  • 一、什么是ForkJoin ForkJoin框架包含ForkJoinTask、ForkJoinWorkerThread、ForkJoinPool 若干...ForkJoinPool 是JDK7引入的线程池,核心思想是将一个大的任务拆分成n个小任务(即fork),然后个小任务处..
  • 虚拟化通过最高层次实现并行机制,提供了利用多核或者多处理器系统的方式 对于软件开发人员说,如何利用个核心增加单个应用程序的吞吐量或者速度是一个问题。 采用并行机制提高单个任务的性能 并行可以完成...
  • 并发编程令人困惑的一个主要原因是 使用并发时需要解决的问题有个 而实现并发的方式也有多种 并且这两者之间没有明显的映射关系(而且通常只具有模糊的界线) 因此 你必须理解所有这些问题特例 以便有效地使用...
  • Python 高性能编程

    千次阅读 2019-04-13 23:30:08
    你将获得 通过阅读本书,你将能够: 更好地掌握 numpy、Cython 剖析器; 了解 Python 如何抽象化底层的计算机架构; 使用剖析手段寻找 CPU ...把进程代码转换到本地或者远程集群上运行; 用更少的内存...
  • CPU、并发、并行、多核线程、进程0、计算机工作流程0.0 基础概念:计算机组成0.1 CPU(Central Processing Unit)(1)控制单元(2)运算单元(3)存储单元:0.2 计算机总线(Bus)0.3计算机工作流程1、线程...
  • Java并发编程最佳实例详解系列

    万次阅读 2018-04-26 20:22:51
    java语言中,线程有四状态:运行 、就绪、挂起结束。 进程是指一段正在执行的程序。而线程有时也被成为轻量级的进程,他是程序执行的最小单元,一个进程可以拥有个线程,各个线程之间共享程序的内功空间...
  • 关于Java高并发编程你需要知道的“升段攻略” 基础 Thread对象调用start()方法包含的步骤 通过jvm告诉操作系统创建Thread 操作系统开辟内存并使用Windows SDK中的createThread()函数创建Thread线程对象 操作系统...
  • Java并发编程_张振华

    2020-08-24 15:36:22
    线程简单实现的三方法2.Thread里面的属性方法3.关于线程的中断机制4.线程的生命周期5.守护线程6.线程组第三章 Thread安全1.初识Java内存模型与线程2.线程不安全与安全3.隐式锁与显示锁4.死锁5.
  • java并发编程

    2021-08-12 09:12:07
    线程可以包含个CPU核心的机器同时处理个不同的任务,优化资源的使用率,提升程序的效率。一些对性能要求比较高场合,线程是java程序调优的重要方面。 Java并发编程主要涉及以下几个部分: 并发编程三...
  • java并发编程的艺术

    2018-08-27 14:35:09
    volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 1.volatile的定义与实现原理 缓存行:缓存中...
  • JUC编程

    2021-02-16 23:36:30
    JUC编程 文章目录JUC编程1.简介2.线程进程线程进程的区别并发、并行线程有几个状态wait/sleep 区别3.Lock锁传统的SynchronizedLock 接口Synchronized Lock 区别4.生产者消费者问题Synchronized生产者消费者...
  • JavaWeb 并发编程 与 高并发解决方案

    万次阅读 多人点赞 2018-09-12 03:41:00
    这里写写我学习到自己所理解的 Java高并发编程和高并发解决方案。现在各大互联网公司中,随着日益增长的互联网服务需求,高并发处理已经是一个非常常见的问题,这篇文章里面我们重点讨论两个方面的问题,一...
  • 网络编程和并发编程面试题

    千次阅读 2020-01-16 11:40:23
    网络编程和并发编程面试题 1.简述 OSI 七层协议。 一、应用层 与其它计算机进行通讯的一个应用,它是对应应用程序的通信服务的。例如,一个没有通信功能的字处理程序就不能执行通信的代码,从事字处理工作的程序员也...
  • 《java并发编程实战笔记》第十一章 性能与可伸展性 第十一章 性能与可伸展性
  • 《Java并发编程的艺术》读书笔记

    千次阅读 2021-11-20 10:08:09
    即使是单核处理器也支持线程处理代码,因为cpu会给每个线程分配时间片,不停的切换线程,让我们感觉线程是同时执行的 线程切换的前,需要保留一个线程任务的状态,以便下一次重新切换回这个任务,所以任务从...
  • JUC 并发编程

    2021-10-02 20:34:45
    java.util.concurrent 并发工具包 用来优雅的解决线程下的高并发问题,JUC 下的大部分类均由Doug Lea 设计开发,此乃神人也! 让 Java 程序员膜拜的大神,这个憨态可掬的老者让人又爱又恨! JUC 的基本框架 2 ...
  • 命令式编程中,线程之间的通信机制有两:共享内存消息传递。​ 共享内存的并发模型中,线程之间共享程序的公共状态,通过写—读内存中的公共状态进行隐式通信。​ 消息传递的并发模型里,线程之间没有公共...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,270
精华内容 1,308
关键字:

多核和多处理器编程,使用哪种编程语言来在单个节点上运行编程代码

友情链接: lab3.zip