『阿男教你玩转Concurrent Programming』*学习的脉络(2)*
为了解决协调并发执行循序
,协调资源的使用
这两个问题,人类做了大量的研究,产生了很多的研究结果,制造了很多解决实际问题的工具。而我们要学习的就是这些东西了。
为了协调并发执行顺序和协调资源的使用,我们需要协调的手段。比如,如何让Worker A
等待Worker B
完成了特定任务后,再继续执行任务?
还比如,如果Worker A
和Worker B
都要使用Resource X
,但是不能同时对Resource X
做操作,怎样才能让它们不同时操作Resource X
?
这种情况下,我们往往使用锁
来实现互斥
,独占
,来完成协调执行顺序
和只允许一个worker
访问某个resource
的工作。
于是,锁
,也就是lock
的实现,如何可靠地获得锁,如何可靠地释放锁,就是一个研究领域。
什么叫可靠地获得锁
?我们可以自己想想。应该可以大概想到一些内容,比如:不能让两个worker
同时获得同一个lock
,那么这个lock
也就没有意义了。所以在lock
的实现方面,我们不能让这种事情发生。
为了避免这种事情发生,最终我在硬件上有保障,保证lock
的可靠性,也就是说,硬件必须要提供atomicity
,原子性。体现在观测结果上,所有worker
都需要观测到一个object
的状态改变
的一致性
。不能说A观测到一个状态的改变,B没观测到,如果是这样的话,锁的实现是无从谈起的。
大家是不是觉得概念上很抽象?但是在软件的实现上,我们经常会和上面所说的atomicity
打交道的。
最常见的就是Java
里面的volatile
这个关键字。如果我们一个变量
不使用volatile
这个关键字,那么Java的虚拟机
可能就不会同步
这个变量
。如果你的程序是multi-threaded
,也就是多线程
的,每一个thread
里面观测到的这个变量
的值可能是不一样的。
为什么?因为层次的丰富性。什么意思?我们的程序面对一个多层的世界:虚拟机,操作系统,CPU。
现代的虚拟机或是操作系统使用的是复杂的虚拟内存实现,为的是效率最大化。对于多线程,使用的是allocation on demanding
的策略。也就是说,能共享的数据,就尽量共享,需要分割的数据,再独立划分空间。
然后是CPU,现代的CPU都提供缓存,提高运行速度,多核CPU下,每颗CPU有自己独立的缓存。你看到的数据是CPU缓存数据,而不是内存里面的实际数据。
于是什么时候CPU更新缓存就成了问题。单线程的程序没有这个焦虑,但是多线程的程序里面,就算是一个Thread更新了一个共享变量
的数据,另一个Thread也不一定能看得到这个改变。
为什么?这里面有好多层要考虑的因素:首先,CPU的缓存也是提供给操作系统指令接口,由操作系统负责更新策略的。
其次,虚拟机在实现的时候,要提供给代码编写者接口控制CPU的缓存更新。比如Java的volatile
关键字。一切为了性能。
但是对于新手,就不知道这些,可能觉得,我在一个thread里面更新了一个共享数据的值,那么在另一个thread里面就可以观测到,其实根本不是。
如果你不使用volatile
关键字,另一个thread读到的可能还是另一个CPU里面的缓存
数据,而不是内存
里面更新后的数据。下图展示了CPU缓存,Thread观测到的数据,以及内存的关系[^1]:
[^1] http://tutorials.jenkov.com/java-concurrency/volatile.html
所以,lock
的实现,要基于所有这些层次丰富的世界进行考量,要考虑到所有的观测不一致的可能性。所以说,软件的锁
的实现,是基于硬件
,操作系统
,虚拟机
一个综合考虑,最终落到一个实处:CPU是一个晶振芯片,时间的原子性。
这次阿男给大家讲了atomicity
和lock
,下次我们再讲基于这两点之上的领域和设计。