int
test_and_set ( volatile int * addr )
// voliatile 告诉编译器,addr指向的地址,可能被别的CPU修改。
// 请不要addr放入寄存器和缓存和优化,避免被别人改了。无法试试拿到最新的值。
// 处理器优化,可能将多余的LOAD和store优化称一个操作。
// 多处理器编程声明变量 volatile 良好编程习惯
{
int old_value;
old_value = swap_atomic( addr,1 );
//原子操作,addr值取出放入寄存器A,
//寄存器B中1放入addr中,
//寄存器A在把值给old_vlaue
if ( old_value == 0 )
return 0 ;
return 1;
}
如果原来addr是0,那么我取到0,现在addr是1. return 0,我知道我拿到了锁,那么我独占使用这个资源。用完后再 test_and_set() 放入0,释放锁。 另外一个人去取addr,return 1 , 设置1(原来是1)。那么我没拿到锁,然后继续重试直到 return 0,为止。 通过这个功能来构建一个应用层的锁,
如果原子操作 swap_atomic() 不是原子的,会怎么样? 这个功能实现的是寄存器和内存中值交换。取出原值,保存寄存器的值到内存中,返回原值。 2个人同时做,非原子,bus调度可能a做到一半,取出0,b开始做,b也取出0,b继续关闭锁赋值1, 然后又切换回a,a也认为自己拿到了锁,然后并发数据结构被破坏,无法得到准确结果。
--------------
并发分析 种类1: 写入a,无论原来是值是什么,直接写a。 不停的覆盖一个值。 读取,不论原来是什么,给我读一下内存。 不停读取。 这种操作,本质上是指令循序的先后,来确定之行的结果。(单线程) 如果多线程,大量这种简单指令(针对一个内存地址),根本无法推测结果,因为他们的先后执行顺序, 根据CPU访问BUS的硬件调度来控制,谁先操作谁后操作无法得知,硬件层次根本不会告诉你, CPU会空闲,你只是拿到结果中间是否发生等待不知道。
---------------------
单纯a线程拿值a1覆盖变量C,单纯b线程拿值b1覆盖变量C, a1和b1都不是基于原来变量C,都是基于自己线程的a1-old,b1-old来的。 a1和b1,都修改C,最多我们看出C最后被谁修改的。b1(1000~5k)和a1(0~999)有明确的取值范围。 这种场景几乎没意义,知道当前开灯关灯,本质上好无意义。 ------------------------------ 计数器应用。 根据上次的结果本次加1,OS提供了原子加一操作。 如果硬件底层实现了,存储+1操作原子,那么os简单包装。 如果硬件没实现,OS必须
--这种场景,除非在app层次有一个地方排队请求,才能确定结果(人工可以推演这个结果)。 --因为我在app层排队了,排队中的顺序如果我知道,我就能预测结果,也就消除了锁的问题把并发变成了排队。
单次操作的顺序问题? 原则应该是谁先执行,多个Cpu执行指令顺序(主机时钟),可以决定执行结果,如果因为访问内存调度的问题, 造成了(BUS在一个时刻只能一个CPU用),先执行的顺序被重新排序了,而这种排序是硬件上的实现的, 人无法预测结果是否为我们预期的。 人可以预期的方式,先发先出结果!
类似于数据库, 补充案例: 不可重复读, 不可幻读。 不可
读改写,原子操作。 明确在并发的时候,保证我执行不会被打断。
我们使用语言的原子操作,sync(),lock()都是对底层CPU指令的封装。 底层CPU指令的封装,又是基于硬件设计的简单化,或者实现的效率考虑。 底层的门电路实现。
~~~~~~~~~~~~~~~~~~~~~~
长期加锁
场景: 内核A进程
这个sleep(walkup)内核函数是针对某个地址发生变化事件,唤醒的函数。 我们平时sleep(时间),时间条件进行让出CPU,唤醒,重新执行。
//flag_ptr 存放某个大结构体中的锁变量-内存位置,
//这个值应该 voliate,多cpu必须访问内存,防止这个变量的并发问题
void lock_object(char * flag_ptr){
while (*flag_ptr)
sleep(flag_ptr);
//如果值是1,一直休眠,除非这个这个锁变量值被改变,被唤醒。
//但是while 而不是if ,是因为while是继续去竞争获得锁,因为N个CPU在等待这个资源释放
//sleep是让出cpu,让cpu干别的事情,
//比纯while(*flag_ptr){ sleep(1)} 睡1秒,这种CPU占用少。
*flag_ptr = 1 ; //上锁
}
但是while( ),这个判断问并发问题(多CPU同时都到判断,然后改变量), 读-改-写 或 测试-设置的措施,来保证这部分的并发。 如果单核并发则没这个问题。
解锁
void unlock_object(char * flag_ptr){
* flag_ptr = 0 ; //解锁
walkup( flag_ptr ); //手工唤醒
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 旁白:
乱序执行技术 编辑 乱序执行(out-of-order execution)是指CPU采用了允许将多条指令不按程序规定的顺 序分开发送给各相应电路单元处理的技术。比方Core乱序执行引擎说程序某一段有7条指令, 此时CPU将根据各单元电路的空闲状态和各指令能否提前执行的具体情况分析后, 将能提前执行的指令立即发送给相应电路执行。
针对同一个存储位置,多个CPU同时读或写,不保证确定次序。也就不确定结果。 多个CPU同时更新同一个位置,破坏数据结构风险。 次序不确定引发竞争。
执行结果,取决多个CPU执行指令,相对次序于时序。
RISC处理器,load-store结构, load从内存取数据,CPU只计算寄存器,store把寄存器计算结果返回到内存。 高速度结构简单。
多处理器编程的艺术
多个CPU同时访问主存储器,总线硬件在多个requestor仲裁,确定一个CPU,CPU允许一次读或写,连续的多个字,用完后释放权限,循环仲裁,找下一个CPU
一个CPU一次读一个高速缓存行,假设传输一个字,一个周期
限制每次读,不能读太多,占用总线太长。为了均匀进程处理速度。
阻塞问题?引申出cpu延时,os无法知晓的
因为总线的带宽-无法满足同时大量数据传输 , 一个cpu读取主存的量,