多线程可以并发执行,可以共享相同的地址空间。同步需要协调各个线程对临界区的访问,在任何时刻只能有一个线程在执行临界区的代码。
一. 信号量
信号量和软件方法不同,线程通过操作系统来协调,各个线程的地位是平等的。
下面是P操作和V操作的内部实现。
整型变量sem是信号量,代表共享的资源数目。
信号量通过PV操作控制线程能否访问临界区(临界区内可以访问共享资源)。P操作导致信号量的值小于0就不能访问临界区并加入将线程等待队列,V操作导致信号量大于等于0就可以让从等待队列取出其他线程来访问临界区。
操作系统保证公平。P操作和V操作执行的原子性是有操作系统保护的,PV操作都是原语,执行中不能被打断。
二. 信号量使用
在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。一个线程执行PV内的代码段时不会被其他线程打断,使得代码段在一个时刻只有一个进程(线程)所拥有。
- 用信号量实现临界区的互斥访问
每个进程都是这套代码,来访问临界区。
对于多个进程,信号量通过PV操作控制线程能否访问临界区(临界区内可以访问共享资源)。
刚开始mutex=1,线程1来了,mutex=0,开始执行临界区;又来了线程2,mutex=-1,加入线程2到等待队列;等到线程1执行结束执行V操作,mutex=0,从等待队列拿出线程2,线程2可以执行。 - 用信号量实现条件同步
线程A要等待线程B执行完X模块,线程A才能执行N模块。 - 用信号量实现生产者消费者问题
对于一个有固定大小的缓冲区,同时共享给两个线程去使用。而这两个线程会分为两个角色,一个负责往这个缓冲区里放入一定的数据,我们叫他生产者。另一个负责从缓冲区里取数据,我们叫他消费者。
Deposit是生产者线程 ,Remove是消费者线程。mutex信号量保证生产者消费者两个线程不会同时操作buffer。emptybuffers信号量保证有空缓冲区后才写数据(emptybuffer=0时访问写入emptybuffer=-1进入等待队列,要等待消费者读出一个emptybuffer=0取出等待线程才能执行写入);fullbuffers信号量保证缓冲区空时生产者线程先运行之后再是消费者线程(fullbuffer=0时访问读出fullbuffer=-1进入等待队列,要等待生产者写入数据fullbuffer=0时取出等待线程才能执行读出)。
不可将前两行对调,否则可能造成缓冲区满时,生产者线程因为mutex拿到执行权却无法向下执行访问缓冲区一直等待死锁。
三. 信号量解决2个问题
1.哲学家就餐问题
两个叉子就是共享资源
每个哲学家就餐线程都是下面的代码,信号量是fork[]数组。左面叉子没了就等着,拿到左面的看右面叉子有无,没有继续等着。
2.读者-写者问题
信号量WriteMutex解决读-写互斥和写-写互斥,有读者就不能写,有写者就不能读,有写者也不能写。但有读者还可以读,信号量CountMutex解决一个读者修改读者计数时,另一个读者不能修改读者计数。
对于多个进程,信号量通过PV操作控制线程能否访问临界区(临界区内可以访问共享资源)。
四. 管程
把分散在各进程中的临界区(访问共享资源)集中起来管理,管程代表共享资源的数据结构及并发进程在其上执行的一组过程。**请求和释放资源的进程会调用管程。**最多只有一个调用者能真正进入管程,其他调用者必须等待直至管程可用。
相对于信号量,管程使用条件变量的同步机制,进程可以在条件变量上等待或被唤醒,可以让阻塞进程放弃管程控制权,在适当时刻再尝试检测管程内状态以便恢复阻塞进程执行。
wait原语:对条件变量执行wait,会引起调用进程阻塞并移入与当前条件变量相关的队列中,让等待在管程之外的一个进程进入管程。
signal原语:如果有其他进程因对条件变量执行wait而被挂起,便唤醒阻塞进程并移出条件变量队列。
有两个条件变量和一个入口等待队列和一个入口锁,count是缓冲区中数据的数目,上图的两个函数是构成管程的代码。线程要使用时调用管程就可以写数据和读数据。
对于每个线程要操作其中一个函数,对于deposit函数先申请管程的互斥访问权(入口锁),如果缓冲区满了,就放弃管程使用权,释放锁资源,等在notfull条件变量上,有空地之后再往里写数据。缓冲区未满就写数据,执行notempty的signal是为了如果有消费者一直等在队列中就把它唤醒了最后释放管程的访问权(锁)。
信号量先检查缓冲区的状态再互斥操作,管程这里倒过来。信号量进入了已经占用缓冲区进行互斥访问别人进不来了,管程检查缓冲区不成功还可以放弃管程的互斥访问权限。
有两种情况
(1)Hoare管程:被唤醒的阻塞进程优先执行。正占用管程执行的线程唤醒阻塞进程后,立即放弃管程的互斥访问权,被唤醒的阻塞进程马上运行。
(2)Hansen管程:正占用管程执行的线程优先执行。唤醒阻塞进程之后,当前进程先执行完释放管程的互斥访问权后,被唤醒的阻塞进程才运行。