并发编程

Jimmy_查查 · · 2998 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

抛砖引玉,同大家一起交流;可能会从两个大方向上来开始:一是来看看为什么并发编程难,包括涉及的一些基础硬件和操作系统相关的一点知识;二是一起来分析一下常同的并发模型,我们怎么通一个简单高效的并发模型来写出简单的并发实现(如线程与锁/actor/csp/);

并发与并行

并发:是逻辑上发生的同时;如一个处理器同时处理多个任务,CPU时间片发生切换

并行:是物理上发生的同时;是多核处理器同时处理多个任务;

系统硬件体系架构

这里做一个假设,如果CPU1在对一个变量执行一个CAS的操作,而该变量的缓存行是在CPU7的高速缓存里面,那么可能的执行顺序可能是:

CPU1检查本地高速缓存,没有找到缓存行;

请求转发到CPU0与CPU1的互联模块,检查CPU0的本地高速缓存,没有找到缓存行;

请求转发到系统互联模块,检查其它三个芯片,得知缓存行被CPU6与CPU7所在的芯片持有;

请求被转发到CPU6和CPU7的互联模块,检查这两个CPU的高速缓存,在CPU7的高速缓存中找到缓存行;

CPU6将缓存行发送给所属的互联模块,并刷新自己高速缓存中的缓存行;

CPU6和CPU7的互联模块将缓存行发送给系统互联模块;

系统互联模块将缓存行发送给CPU0和CPU1的互联模块;

CPU0和CPU1的互联模块将缓存行发送给CPU1的高速缓存;

CPU1对高速缓存中的变量执行CAS操作;

这是一个简化并忽略了某些复杂的事件序列,因为:

其它CPU可能试图在相同的缓存行上执行并发的CAS操作;

缓存行可能被只读复制到其它的CPU高速缓存中,这种情况下有必要刷新它们的缓存;

当请求到达时,CPU7可能已经在缓存上操作,这种情况下CPU7必须你保留这个请求,直到请求完成、

CPU7可能已经从缓存中排出它的缓存行,这样当请求到达时,缓存行已经写入内存中了;

在缓存行中可能发生一个可纠正的错误,因此需要在使用数据前纠正它;

...

CPU的缓存一致性极其复杂,所以高效率(榨干最后一滴CPU资源,每瓦特性能)且可靠的并行编程总是太不容易!

除了CPU高速缓存还有许多其它因素,如内存引用、原子操作、内存屏障、I/O操作等等。

内存引用

微处理器从内存里读一个值的时间,微处理器可以用这段时间执行成成百甚至上行知指令,虽然一直在极大的减少内存访问的延迟,但是仍只有高度可以预测的数据访问模式才能让缓存发挥最大效果;

原子操作

CPU会通过一条『流水线』来控制CPU内部的指令流(现在微处理器都可以支持多条流水线并行),这种架构使得CPU流水线的可以一次执行多个操作,而原子操作正与这种特性有冲突;比如一种常见的技巧是标出所有包含原子操作所需数据的流水线,保证CPU在操作时,这些流水线都属于正在执行原子操作的CPU;如果我们按CPU一个时钟周期执行一条指指令(约为0.6ns),一个最好情况下的CAS操作也需要40个时钟周期;

内存屏障

这个比较好理解,一般有Load Barrier 和 Store Barrier即读屏障和写屏障,如JAVA中的volatile关键字

寄存器在执行前,为了提高性能,会对指令重排,而内存屏障会禁止指令重排;

强制将CPU高速存中的数据写回内存,让缓存中相关的数据失效,这又涉及到内存总线 CPU与内存的读写等等的性能损耗;

JVM对内存屏障的详细内容可以参考一下<>,作者是阿里大神方腾飞,也是 并发编程网 博主,总之很牛逼。

I/O操作

如高速缓存未命中(CPU之间的I/O),如果涉及到网络,大容量存储(磁盘),这类操作对于性能的影响更是远远大于上面提到几种的开销;

所以并行编程变得复杂,除了与硬件的交互、还有任务分割、并行访问控制等;这些如线程、锁、屏障等,我们在实际工作中应尽量避免直接控制它们,因为它们都有怪脾气,处处是陷井;庆辛随着为并发设计编程语言的兴起(Elang,Scala,Golang等),在释放多核威力的同时,也一定程度降低了并发程序的难度。特别Golang也是Docker这类明星产品的实现语言。

Golang并发模式(CSP)并发内核

(注,该图引自网络)

其中M是一个内核线程,P是调度器,G是一个协程,灰色的G为挂起的协程;Go通过协程goroutine提供语言层面的调度器,实现高效的M:N(M个用户线程对应N个os内核线程)对应关系,使用goroutine做为并发实体,非常的轻量级,理论可以很轻松的创建上十万个goroutine。

P作为调度器,作用类似于CPU的核,每个工作线程都必须绑定一个有效的P才被允许执行,否则只能休眠等到有空闲的P时被唤醒;P还要为线程提供执行资源,如为对象分配内存 本地任务队更等;

而实际的执行体是M(OS内核线程),和P绑定 不停的获取执行G的并发任务。M通过修改寄存器,将执行栈指向G自带的栈内存;

P/M是执行的组合体,但是两者数量并非是一一对应,M由调度器按需创建,比如当M陷于一个IO操作长时间阻塞,P就会被监控线程抢回去去创建或唤醒一个M执行其它任务。

如果想深入了解,建议可以看看雨痕写的 <<源码解析>> 郁闷的点是有汇编和C的代码...

从一段简单的代码来看

主协程调用producer,创建了个生产者协程,并返回了一个通道,这里也可以理解为一个服务。

主协程的do_something执行业务逻辑。

JAVA的同学从简单理解就当chan就是JAVA中的队列(用通讯的方式共享内存)

先做抛砖引玉,后续再专门针对Go 和Actor做一些分享,交流。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:Jimmy_查查

查看原文:并发编程

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2998 次点击  
加入收藏 微博
下一篇:B 树
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传