1. 互斥锁(解决进程互斥问题)
解决临界区临界资源冲突的最简单的方式就是通过互斥锁。一个进程在进入临界区的时候加锁,出临界区时解锁。解决进程互斥问题
假设通过release函数释放锁资源,acquire函数获得锁
其中,每个互斥锁都会有一个变量available,这个变量标记这个锁现在是否是可用的。如果进程申请不可用锁会阻塞。直到锁被释放
伪代码如下:
acquire (){
while(!available);//忙等待
available = false;//获得锁
}
release() {
available = true;//释放锁
}
需要注意:
release和acquire这两个函数都需要原子性操作,因此通常采用硬件机制来实现
缺点:
- 在锁没有就绪时,CPU处于忙等状态。浪费CPU资源。不太适合单处理机
优点:
- 互斥锁通常用于多处理机上,进程在一个处理机上等待,不会影响另一个进程的运行。等待消耗比进程切换的代价更低。
需要连续循环忙等的互斥锁,都可称为自旋锁(spin lock),如TSL指令、swap指令、单标志法
2. 信号量
用户可以通过系统提供的一对原语对信号量进行操作,实现进程的同步互斥
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量)。
可以用一个信号量来表示系统中某种资源的数量。
eg:比如:系统中只有一台打印机,就可以设置一个初值为1的信号量
这一对原语为:
wait(S) signal(S)原语 S是信号量
wait(S)又称信号量的P操作,使计数器-- P(S)
signal(S)又称信号量的V操作,使计数器++V(S)
整型信号量
用一个整数来表示系统资源的数量,对信号量的操作只有三种。
初始化,P操作,V操作
伪代码:
int s = 1; //初始化整型信号量s,表示当前系统中可用的打印机资源数
//P操作
void wait (int s) { // wait原语,相当于“进入区”
while (s <= 0);//如果资源数不够,就一直循环等待,会出现忙等状态
s=s-1;//如果资源数够,则占用一个资源
}
//V操作
void signal (int s) { // signal原语,相当于“退出区”
s=s+1; //使用完资源后,在退出区释放资源
}
问题:会发生忙等状态。
记录型信号量
整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表示的信号量。
伪代码:
/*记录型信号量的定义*/
typedef struct {
int value;//剩余资源数
struct process *L;//等待队列
}semaphore;
//P操作:
/*某进程需要使用资源时,通过wait原语申请*/
void wait ( semaphore s) {
s.value--;
if(s.value < 0){
block (s.L);//block原语,把当前进程阻塞,将其挂到等待队列中
}
}
//V操作:
/*进程使用完资源后,通过signal原语释放*/
void signal (semaphore s) {
s.value++;
if (s.value <= 0) {
wakeup(s.L);//wakeup原语,从等待队列中唤醒某个进程,该进程从阻塞态变为就绪态
}
}
优点:
- 记录型信号量可以实现系统资源的申请与释放。
- 可以使用记录型信号量实现进程同步与互斥。
3. 信号量实现同步与互斥
首先,先明确信号量的定义:
信号量的值=这种资源的剩余数量(信号量的值如果小于0,说明此时有进程在等待这种资源)
P(S)一申请一个资源s,如果资源不够就阻塞等待
V(S)―释放一个资源s,如果有进程在等待该资源,则唤醒一个进程
信号量实现进程互斥
伪代码:
/*信号量机制实现互斥*/
semaphore mutex=1;//初始化信号量
P1(){
……
P(mutex);
//使用临界资源前需要加锁
临界区代码段...
V(mutex);
//使用临界资源后需要解锁
……
}
P2(){
……
P( mutex) ;
临界区代码段...
V(mutex);
……
}
具体实例:
Linux-Centos环境下C++ 封装信号量:
#include<iostream>
#include<semaphore.h>
#include<pthread.h>
#include<unistd.h>
class Sem//封装信号量
{
public:
Sem(int Num)
{
sem_init(&sem,0,Num);//初始化信号量,0表示线程间共享,非0表示进程间共享(基本不用)
}
void P()//使计数器--
{
sem_wait(&sem);
}
void V()//使计数器++
{
sem_post(&sem);
}
~Sem()
{
sem_destroy(&sem);
}
private:
sem_t sem;
};
用三个线程测试互斥操作
Sem sem(1);//初始化信号量的值为1
//考试中初始化信号量的方式默认为semaphore mutex=1;
int cout=1000;
void*Run(void*arg)
{
char*Str=(char*)arg;
while(true)
{
sem.P();//申请到资源
if(cout>0){
usleep(1000);
cout--;
std::cout<<Str<<"make cout-- cout="<<cout<<std::endl;
sem.V();//释放资源
}
else{
sem.V();//如果cout为0说明此时应该退出循环,释放资源后退出
break;
}
}
std::cout<<Str<<"quit"<<std::endl;
pthread_exit((void*)1);
}
int main()
{
pthread_t tid1,tid2,tid3;
pthread_create(&tid1,nullptr,Run,(void*)"pthread1");
pthread_create(&tid2,nullptr,Run,(void*)"pthread2");
pthread_create(&tid3,nullptr,Run,(void*)"pthread3");
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
pthread_join(tid3,nullptr);
return 0;
}
信号量实现进程同步
进程同步:要让各并发进程按要求有序地推进。
首先要分析在什么时候要实现进程同步,即必须保证“一前一后”执行的两个操作(或两句代码)
设置同步信号量S,初始值为0
伪代码:
/*信号量机制实现同步*/
semaphore S=0;//初始化同步信号量,初始值为0
P1(){
代码1;
代码2;
V(S);//信号量+1,唤醒阻塞的进程
代码3;
}
P2(){
P(S);
代码4;
代码5;
代码6;
}
//实现了代码4必须在代码2之后执行的目的。
如果代码1,代码2没有执行,P2会阻塞在P(S)上,没法运行代码4。
只有代码1,代码2执行后,V(S)唤醒P2进程,P2进程才执行代码4
简单的理解就是:前V后P,先执行的进程V操作,后执行的进程P操作等待先执行的进程V操作。