临界区与锁
进程同步
进程同步也是进程之间直接的制约关系,进程间的直接制约关系来源于他们之间的合作。比如说进程A需要从缓冲区读取进程B产生的信息,当缓冲区为空时,进程B因为读取不到信息而被阻塞。而当进程A产生信息放入缓冲区时,进程B才会被唤醒。
进程互斥
进程互斥是进程之间的间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。
实现进程同步和互斥的基本方法
法I:硬件实现方法——关CPU的中断。CPU进行进程切换是需要通过中断来进行,如果屏蔽了中断那么就可以保证当前进程顺利的将临界区代码执行完,从而实现了互斥。这个办法的步骤就是:屏蔽中断–执行临界区–开中断。但这样做并不好,这大大限制了处理器交替执行任务的能力。并且将关中断的权限交给用户代码,那么如果用户代码屏蔽了中断后不再开,那系统岂不是跪了?
法II:软件实现方式——信号量
这也是我们比较熟悉P V操作。
- P:荷兰语Passeren,表示占有(资源)。
- P(S):①将信号量S的值减1,即S=S-1;②如果S>=0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
- V:荷兰语Vrijgeven,表示释放(资源)。
- V(S):①将信号量S的值加1,即S=S+1;②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。
- S: Semaphore,信号量。当S>0时,S表示当前可用资源的数量;当S<0时,其绝对值表示等待使用该资源的进程个数。
S如果只能=0或1,表示对于临界区的保护,这时的P操作和V操作有以下两种实现方式
- critical section(临界区)
- mutex(互斥器)
定义写法 |
HANDLE hmtx; |
CRITICAL_SECTION cs; |
初始化写法 |
hmtx= CreateMutex (NULL, FALSE, NULL); |
InitializeCriticalSection(&cs); |
结束清除写法 |
CloseHandle(hmtx); |
DeleteCriticalSection(&cs); |
无限期等待的写 法(P操作) |
WaitForSingleObject (hmtx, INFINITE); |
EnterCriticalSection(&cs); |
0等待(状态检测) 的写法(P操作) |
WaitForSingleObject (hmtx, 0); |
TryEnterCriticalSection(&cs); |
任意时间等待的 写法(P操作) |
WaitForSingleObject (hmtx, dwMilliseconds); |
不支持 |
V操作的写法 |
ReleaseMutex(hmtx); |
LeaveCriticalSection(&cs); |
优点 |
可以跨进程 |
速度快。因为Critical Section不是内核对象,函数EnterCriticalSection()和LeaveCriticalSection()的调用一般都在用户模式内执行。 |
缺点 |
速度慢。Mutex 是内核对象,相关函数的执行 (WaitForSingleObject,ReleaseMutex)需要用户模式(User Mode)到内核模式(Kernel Mode)的转换,在x86处理器上这种转化一般要发费600个左右的 CPU指令周期。 |
只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。 |
例1:生产者和消费者的关系
class ProducerAndCustomer { private static Mutex mut = new Mutex(); //临界区信号量 private static Semaphore empty = new Semaphore(5, 5); private static Semaphore full = new Semaphore(0, 5); static void Main() { Thread Thread1 = new Thread(new ThreadStart(Producer)); Thread Thread2 = new Thread(new ThreadStart(Customer)); Thread1.Start(); Thread2.Start(); } private static void Producer() { empty.WaitOne(); //对empty进行P操作 mut.WaitOne(); //对mut进行P操作(拿走一个空瓶子) //数据放入临界区... Thread.Sleep(12000); mut.ReleaseMutex(); //对mut进行V操作 full.Release(); //对full进行V操作(放入一个满瓶子) } private static void Customer() { Thread.Sleep(12000); full.WaitOne();//对full进行P操作 mut.WaitOne(); //对mut进行P操作 //读取临界区... mut.ReleaseMutex(); //对mut进行V操作 empty.Release(); //对empty进行V操作 } }
例2:读者–写者问题
规定:允许多个读者同时读一个共享对象,但禁止读者、写者同时访问一个共享对象,也禁止多个写者访问一个共享对象
分析:这里的读者和写者是互斥的,而写者和写者也是互斥的,但读者之间并不互斥。由此我们可以设置3个变量,一个用来统计读者的数量,另外两个分别用于读写的互斥,写者和写者的互斥。以下程序假设只有一个写程序,所以没有声明写者与写者互斥的信号量。class ReaderAndWriter { private static Mutex mut = new Mutex();//用于保护读者数量的互斥信号量 private static Mutex rw = new Mutex(); //保证读者写者互斥的信号量 static int count = 0;//读者数量 static void Main() { for(int i = 0; i < 5; i++) { Thread Thread1 = new Thread(new ThreadStart(Reader)); Thread1.Start(); } Thread Thread2 = new Thread(new ThreadStart(writer)); Thread2.Start(); } private static void Reader() { mut.WaitOne(); if (count == 0) { rw.WaitOne(); } count++; mut.ReleaseMutex(); Thread.Sleep(new Random().Next(2000)); //Read... mut.WaitOne(); count--; mut.ReleaseMutex(); if (count == 0) { rw.ReleaseMutex(); } } private static void writer() { rw.WaitOne(); //Write... rw.ReleaseMutex(); } }
例3:哲学家进餐问题
有五个哲学家,他们的生活方式是交替地进行思考和进餐。他们公用一张圆桌,在圆桌上有五个碗和五根筷子,哲学家饥饿时便试图取用其左、右最靠近他的筷子,只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。
class philosopher { private static int[] chopstick=new int[5];//分别代表哲学家的5只筷子 private static Mutex eat = new Mutex();//用于保证哲学家同时拿起两双筷子 static void Main() { //初始设置所有筷子可用 for (int k = 0; k < 5; k++) chopstick[k]=1; //每个哲学家轮流进餐一次 for(int i=1;i<=5;i++){ Thread Thread1 = new Thread(new ThreadStart(Philosophers)); Thread1.Name = i.ToString(); Thread1.Start(); } } private static void Philosophers() { //如果筷子不可用,则等待2秒 while (chopstick[int.Parse(Thread.CurrentThread.Name)-1] !=1 || chopstick[(int.Parse(Thread.CurrentThread.Name))%4]!=1) { Thread.Sleep(2000); } eat.WaitOne(); //同时拿起两双筷子 chopstick[int.Parse(Thread.CurrentThread.Name)-1] = 0; chopstick[(int.Parse(Thread.CurrentThread.Name)) % 4] = 0; eat.ReleaseMutex(); Thread.Sleep(1000); //正在用餐... chopstick[int.Parse(Thread.CurrentThread.Name)-1] = 1; chopstick[(int.Parse(Thread.CurrentThread.Name)) % 4] = 1; } }
spin lock(自旋锁)
自旋锁是专为防止多处理器并发而引入的一种锁。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。