在操作系统理论课上,其实讲授了信号量的原理和使用方式以及使用信号量的优点。相信看到这篇文章的人已经对信号量底层实现机制有了一定的了解,这里就不再过多赘述。本文主要以两个题目为例来讲授信号量如何在高级语言中使用。如果不想费力去弄懂信号量,又想要写并发程序,可以参考go语言。
goroutine机制https://blog.youkuaiyun.com/prestyan/article/details/124366846?spm=1001.2014.3001.5501
与信号量紧密相关的两个操作是P,V操作,一些书中还会以wait(s),signal(s)作为示例的伪代码。但是真正要在程序当中使用信号量,其实与所使用的高级语言有关。能不能使用信号量来实现程序并发运行而不出差错,要看使用的语言中有没有提供该函数。如果没有提供,是没办法使用的。并且,所使用的函数形式还与操作系统相关。
一个典型的信号量抽象概念:
typedef struct semaphore
{
int value;
struct process_control_block *lists;
}semaphore;
wait(semaphore *s)
{
s->value--;
if(s->value<0)
block(s->lists);
}
signal(semaphore *s)
{
s->value++;
if(s->value<=0)
wakeup(s->lists)
}
如果高级语言没有定义或者所给的函数不符合我的预期,可否使用上面的伪代码自定义信号量以及其相关操作?
很明显是不可以的。
因为用户定义的并非原子操作或者说是原子指令,执行过程中达不到想要的目标。除非使用锁操作lock,或者关中断。
_asm("cli");//关中断
codes;
_asm("sti");//开中断
但是效率太低,而且使用了特权指令,不推荐。
C语言中其实提供了非常高效的记录型信号量的函数,如下面第一个例子。本代码都运行在windows环境下。
编写程序模拟场景:
桌上有一空盘,只允许存放一个水果。爸爸专向盘中放橙子,妈妈专向盘中放苹果,女儿专等吃橙子,儿子专等吃苹果。规定当盘空时一次只能放一个水果供吃者自用,请用P,V操作实现爸爸、妈妈、女儿、儿子四个并发进程的同步。
可以推断有四个线程公用一个缓冲区(plate),两个为写(放水果)进程,两个为读进程(拿水果),并且涉及到了生产者与消费者的同步问题。四个进程两两都不能同时进行临界区操作(访问盘子)。因此,需要三个信号量,一个用于控制写进程访问临界区,另外两个用于通知各自写进程对应的读进程临界区满,即放上了相应的水果。
//信号量句柄
HANDLE put,geta,geto;
//线程函数
void pthread_put_orange(void *arg);
void pthread_put_apple(void *arg);
void pthread_get_orange(void *arg);
void pthread_get_apple(void *arg);
C语言中句柄可以为pthread,process或者semaphore等。相当于句柄就是具体的信号量。
根据分析,盘子只有一个,因此put信号量初始值设置为1;其余两个信号量初值为0,表示开始没有资源。下面展示了信号量初始化。
void initSemaphore()
{
printf("Plate Size: %d\n",plate_size);
printf("Plate initial situation: Empty\n");
printf("Main thread wait 10s.\n");
put=CreateSemaphore(NULL,1,1,NULL);//初始有一个空盘子
geta=CreateSemaphore(NULL,0,1,NULL);//没有apple,需要等待puta完毕
geto=CreateSemaphore(NULL,0,1,NULL);//没有orange,需要等待puto完毕
plate=EMPTY;
}
首先,两个读进程要抢占put信号量,并往里放水果。放完后需要通知相应写进程缓冲区满,写进程从阻塞变为就绪执行完毕后,释放put信号量。注意,put信号量由写进程释放。下面展示了信号量的P,V操作。
void pthread_put_orange(void *arg)
{
while(1)
{
WaitForSingleObject(put,INFINITE);//P(put)
plate=ORANGE;
printf("%s\n","-->Father put 1 orange in plate.");
ReleaseSemaphore(geto,1,NULL);//V(geto)
Sleep(1000);
}
}
写进程收到通知后执行拿水果操作。如果写进程先拿水果,则会被阻塞(get=0),以此实现了题目要求。
void pthread_get_orange(void *arg)
{
while(1)
{
WaitForSingleObject(geto,INFINITE);//P(geto)
plate=EMPTY;
printf("%s\n","-->Daughter get 1 orange from plate.");
ReleaseSemaphore(put,1,NULL);//V(put)
}
}
整个代码的结构如下:
father()
{
P(plate);
put orange;
V(o);
}
mother()
{
P(plate);
put apple;
V(a);
}
daughter()
{P(o);get orange;V(put);}
son()
{P(a);get apple;V(put);}
这样就全部实现了题目要求。
题目2:
假设有个南北向的桥,仅能容同方向的人顺序走过,相对方向的两个人则无法通过。现在桥南北端都有过桥人。现把每个过桥人当成一个进程,用P,V操作实现管理。
这个题目与上题类似,不一样的是,如果已经有一个进程抢占了临界区,与之相同类型的进程可以直接进入临界区而无需执行P操作。
代码结构如下:
semaphore b=1,s=1,n=1;
int s2n=0,n2s=0;
ps2n()
{
P(s);
if(s2n==0){P(b);}
s2n++;
V(s);
corss bridge;
P(s);
s2n--;
if(n2s==0){V(b);}
V(s);
}
ps2n()
{
P(n);
if(n2s==0){P(b);}
n2s++;
V(n);
corss bridge;
P(n);
n2s--;
if(n2s==0){V(b);}
V(n);
}
读者可以自行实现该题目。
完整代码地址如下: