critical section 的情景如下。多个程序会对共享的变量进行操作由于对于变量的操作不是原子性的(不可分割,不能被中断)就会造成数据的不一致。举个例子,例如你的银行卡上有 100 块钱,如果你同时在商家 A 与商家 B 同时消费 100 元的东西。如果记账的过程不是原子性的。商家需要先发请求到银行用户在这里想要消费100元,银行后台程序需要看看用户的账户里面是否钱是充足的,如果钱是充足的就批准商家的消费请求。由于计算机体系结构的原因,memory hierarchy,与操作系统的分时处理,如果没有一定的同步措施,数据不一致的情况。在加上现代的程序架构会使用多线程,多进程的方式解决问题。会出现这样的情况,商家 A B 同时发出请求,两个子程序同时从数据库中读取到用户账号里面有 100 元,因此同时批准了这两笔消费。总计消费了 200 元,实时上用户账号只有 100 元,这时就会出现冲突。
在单核的情况下,由于多线程多进程是共享cpu计算时间,程序 a 读到账号上有 100 元钱, 还没进行处理,操作系统让 cpu 执行 b,b 也读到用户有 100 元。此时操作系统让 a 程序继续执行,并且扣除了用户账号上面的余额,之后操作系统让程序b继续执行,由于此时程序b不会再去读取用户的账户所以这个程序也会批准这个交易进行。这样用户就会平白无故的赚了100元。
如果是在多核或者是多服务器的情况下,由于 a,b程序可能会同时执行,他们同时读取到用户的账号上面有 100 元,并且同时批准了交易。造成冲突的原因与单核情况下不太一致。
如果站在更高角度上看是由于不同程序间的数据同步问题造成的这一系列冲突,不同程序修改的变量无法同步到不同的程序中。如果站在底层看就是由于 memory hierarchy 造成同一个数据会被放在不同地方。尽管有缓存一致性协议,但是由于操作不是原子性的所以不同的程序不知道对方程序再干嘛所以会造成冲突。
这就是 critical section 的场景。
那么如何解决这样的问题呢?一种方法是通过一定的机制使得在同一时间只有一个程序能够对用户的账号进程操作。Peterson's Solution 算法 就是一种在 单核并发
的解决方案。注意这里是单核。
bool flag[2];
int turn;
// i process
do{
flag[i] = true;
turn = j;
while(flag[j] && turn == j); // do nothing
//critical section
flag[i] = false;
}
// j process
do{
flag[j] = true;
turn = i;
while(flag[i] && turn == i); // do nothing
//critical section
flag[j] = false;
}
fage[i] == true
时,表示 程序 i 准备进入 critical section了,而 turn 则表示某个程序正在 critical section。那这个算法如何保证同时只有一个程序在critical section里面呢?这里有一个前提假设就是 load and write 是原子性的。所以同一时刻,turn 要么是 i 要么是 j。所以 如果两个程序都是 ready 的,但是只有一个程序能够进入 critical section,另一个程序会一直循环等待。在这里的 turn 变量的唯一性(不同程序读到的都是一样的)很重要。
但是如果换到多核情况就会出现, i 和 j 同时读到 turn == i
turn == j
。此算法就会失效。