TSL指令
类似于java的CAS。
Test and set lock.这是一个原子操作,他的读写操作是不分开的。
TSL指令实现锁机制:当lock为0时,任何进程都可以使用TSL指令将其设置为1,然后访问临界区,操作结束时,再将lock重新设置为0.
1 | void acquire(int *lock){ |
在acquire函数中,如果TestAndSet返回1,那么while循环就一直执行(也就是在这里等待),直到另一个线程调用release。当然,这个实现看起来不太好,主要是等待的线程会不停的检查,浪费CPU,这个问题称之为忙等待(busy-wait or spin-wait),所以这个lock的实现也叫自旋锁spinlock。解决办法是如果需要等待,那么该线程主动交出执行权,让其他线程有机会执行,这种方式称之为让权等待(yield-wait or sleep-wait),应用开发人员使用的互斥锁一都是指这种情况。
以上的这些机制都是忙等待。
当一个进程想进入临界区,先检查是否允许进入,若不允许,将会原地等待,直到允许为止。
浪费CPU。
信号量
可以同时多个线程访问临界区,有P,V两个原子操作。
P(): 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
V(); 对信号量执行 +1 操作,唤醒睡眠的进程让其完成 P操作。
要是信号量的取值变为了1,那么就变成了互斥量。
1 | mutex=new Semaphore(1) |
信号量主要可以解决两类问题。
- 互斥问题。同一时刻只可以有一个线程访问某一个临界资源。
- 同步问题。线程A需要等待线程B执行完毕后才可以继续执行。
信号量解决生产者消费者问题
- 同一时刻,只能有一个生产者或者是消费者访问缓冲区。(互斥问题)
- 缓冲区满时,生产者需要等待消费者消费。(同步问题)
- 缓冲区空时,消费者需要等待生产者生产。(同步问题)
1 | //互斥量 |
注意,empty->P()和mutex->P()不可以交换顺序。
要是
1 | mutex->P() |
假设我们现在的empty已经是0了,mutex先执行也变为了0,但是当执行到下一步empty-P(),发现自己需要阻塞,但是mutex还未释放,会造成死锁。
两个V()操作可以交换顺序。
管程(Monitor)
管程=互斥量+条件变量。
互斥量:可以保证共享资源在同一时间只能有一个进程访问。
条件变量:正在管程内的线程可以放弃对管程的控制权,等待某些条件发生再继续执行。
每个条件变量实际上代表的是一个等待队列。
当wait时,进程释放锁,挂起,并插入该条件变量的等待队列。
signal时,唤醒条件变量等待队列中的进程。
任意时刻管程中只能有一个活跃进程。
条件变量:解决死锁问题,挂起进程。
条件变量。wait:释放锁,挂起 notify:唤醒等待队列中一个线程。防止死锁。
管程解决生产者消费者问题
1 | mutex buffer;//互斥量,一次只能由一个进程访问 |