6.2 临界区问题
临界区问题的解答必须满足以下三项要求:
1. 互斥. 不能有两个线程同时在临界区内执行
2. 前进. 当临界区为空, 而一个线程希望进入临界区时, 该线程进入临界区
3. 有限等待. 一个线程从申请进入临界区到真正进入临界区这段时间不能无限长.
6.3 Peterson 算法
一种软件的方法解决死锁问题, 但也并不能总是解决死锁问题
定义两个变量, 分别为 flag[0/1], turn. flag 表示哪个线程有进入临界区的意愿, turn 表示哪个线程
while(true) { flag[i] = true; turn = j; while(flag[j] && turn == j); // let j run first // cirtical section flag[i] = false; // remainer section }
证明满足上面 3 个要求
1. 互斥. 假设两个线程同时进入临界区, 那么 flag[i] == flag[j] == 1. 当 turn = j 时, 线程 i 在自旋, 不可能进去.
2. 前进. 当线程 j 没有意愿进入临界区时, flag[j] = false; 当 i 有意愿进去时, 直接就进入了.
3. 有限等待. 首先申请临界区的线程进入临界区, 一个线程至多等待另一个线程在临界区执行一次, 满足有限等待.
6.4 硬件同步.
使用原子函数 setAndSwap()
void getAndSet(bool var) { swap(lock, var); }
上面的代码核心是 getAndSet 必须时原子操作. 当 lock 为 false 时, 线程上锁进入临界区. 当 lock 为 true 时, 那么 getAndSet 无限循环, 直到 lock 为 false;
6.5 信号量与死锁
acquire() { value --; if(value < 0) { add this process to list block; } } release() { value ++; if(value <= 0) { remove a process P from list wakeup(P); } }
当 value 为负时, value 的绝对值对应被阻塞线程的数目.
死锁
S.acquire(); Q.acquire(); Q.acquire(); S.acquire(); ... ... S.release(); Q.release(); Q.release(); S.release();
上面代码中, P0 执行 S.acquire(), P1 执行 Q.acquire, 然后 P0 执行 Q.acquire, 最后 P1 S.acquire.
然后... 就死锁了
6.6 经典同步问题
1. 有限缓冲区问题
生成者通过 insert 函数向缓冲区内添加 item, 消费者通过 remove 函数在缓冲区内删除 item
信号量有 3 个, 分别为 empty, full, mutex. empty 表示缓冲区内的空格位置, insert 时需要检查缓冲区是否为空, remove 时需要检查缓冲区是否含有元素. mutex 提供对缓冲区的互斥访问
semaphore empty, full, mutex; // init empty = size, full = 0, mutex = 1; // java code public void insert(Object item) { empty.acquire(); mutex.acquire(); buffer[in] = item; in = (in+1) % BUFFER_SIZE; mutex.release(); full.release(); } public Object remove() { full.acquire(); mutex.acquire(); Object item = buffer[out]; out = (out + 1) % BUFFER_SIZE; mutex.release(); empty.release(); return item; }
注意, empty, full 都必须使用信号量来表示其剩余个数, 不能用 if 代替
2. 读者写者问题
// writer may starve void read() { while(true) { mutext.acquire(); readCount ++; if(readCount == 1) { writeLock.acquire(); } mutext.release(); do reading mutext.acquire(); readCount --; if(readCount == 0) { writeLock.release(); } mutext.release(); } } void write() { writeLock.acquire(); do writing writeLock.release(); }
6.7 管程
管程将 acquire, release 这些操作封装起来了. 管程确保一次只有一个进程能在管程内活动.
管程内的条件遍历有两个操作:
wait() 挂起调用进程并释放管程, 直至另一个进程在条件变量上执行 signal()
signal() 假如有因条件变量被挂起的线程, 那么释放之, 否则什么也不做.
管程实现生产者消费者问题
// full means that no space for new added item // empty means that no item for consumer monitor ProducerConsumer { int itemCount; condition full; condition empty; procedure add(item) { if(itemCount == BUFFER_SIZE) { wait(full); } putItemIntoBuffer(item); itemCount ++; if(itemCount == 1) { signal(empty); } } procedure remove() { if(itemCount == 0) { wait(empty); } removeItemFromBuffer(); itemCount --; if(itemCount == BUFFER_SIZE-1) signal(full); } }
管程示意图