线程同步——信号量

信号是 E. W. Dijkstra 在二十世纪六十年代末设计的一种编程架构。Dijkstra 的模型与铁路操作有关:假设某段铁路是单线的,因此一次只允许一列火车通过。信号将用于同步通过该轨道的火车。火车在进入单一轨道之前必须等待信号灯变为允许通行的状态。火车进入轨道后,会改变信号状态,防止其他火车进入该轨道。火车离开这段轨道时,必须再次更改信号的状态,以便允许其他火车进入轨道。

在计算机版本中,信号以简单整数来表示。线程等待获得许可以便继续运行,然后发出信号,表示该线程已经通过针对信号执行 P 操作来继续运行。线程必须等到信号的值为正,然后才能通过将信号值减 1 来更改该值。完成此操作后,线程会执行 V 操作,即通过将信号值加 1 来更改该值。这些操作必须以原子方式执行,不能再将其划分成子操作,即,在这些子操作之间不能对信号执行其他操作。在 P 操作中,信号值在减小之前必须为正,从而确保生成的信号值不为负,并且比该值减小之前小 1。在 PV 操作中,必须在没有干扰的情况下进行运算。如果针对同一信号同时执行两个 V 操作,则实际结果是信号的新值比原来大 2。

对于大多数人来说,如同记住 Dijkstra 是荷兰人一样,记住 PV 本身的含义并不重要。但是,从真正学术的角度来说,P 代表 prolagen,这是由 proberen te verlagen 演变而来的杜撰词,其意思是尝试减小V代表 verhogen,其意思是增加。Dijkstra 的技术说明 EWD 74 中介绍了这些含义。

sem_wait(3RT) 和 sem_post(3RT) 分别与 Dijkstra 的 PV 操作相对应。sem_trywait(3RT) 是 P 操作的一种条件形式。如果调用线程不等待就不能减小信号的值,则该调用会立即返回一个非零值。

有两种基本信号:二进制信号和计数信号量。二进制信号的值只能是 0 或 1,计数信号量可以是任意非负值。二进制信号在逻辑上相当于一个互斥锁。不过,尽管不会强制,但互斥锁应当仅由持有该锁的线程来解除锁定。因为不存在“持有信号的线程”这一概念,所以,任何线程都可以执行 Vsem_post(3RT) 操作。

计数信号量与互斥锁一起使用时的功能几乎与条件变量一样强大。在许多情况下,使用计数信号量实现的代码比使用条件变量实现的代码更为简单。但是,将互斥锁用于条件变量时,会存在一个隐含的括号。该括号可以清楚表明程序受保护的部分。对于信号则不必如此,可以使用并发编程当中的 go to 对其进行调用。信号的功能强大,但是容易以非结构化的不确定方式使用。

1、命名信号量和未命名信号量

  POSIX信号可以是未命名的,也可以是命名的。未命名信号在进程内存中分配,并会进行初始化。未命名信号可能可供多个进程使用,具体取决于信号的分配和初始化的方式。未命名信号可以是通过fork()继承的专用信号,也可以通过用来分配和映射这些信号的常规文件的访问保护功能对其进行保护。命名信号类似于进程共享的信号,区别在于命名信号是使用路径名而非pshared值引用的。命名信号可以由多个进程共享。命名信号具有属主用户ID、组ID和保护模式。对于open、retrieve、close和remove命名信号,可以使用以下函数:sem_open、sem_getvalue、sem_close和sem_unlink。通过使用sem_open,可以创建一个命名信号,其名称是在文件系统的名称空间中定义的。

2、计数信号量概述

  从概念上来说,信号量是一个非负整数计数。信号量通常用来协调对资源的访问,其中信号计数会初始化为可用资源的数目。然后,线程在资源增加时会增加计数,在删除资源时会减小计数,这些操作都以原子方式执行。如果信号计数变为零,则表明已无可用资源。计数为零时,尝试减小信号的线程会被阻塞,直到计数大于零为止。

  由于信号无需由同一个线程来获取和释放,因此信号可用于异步事件通知,如用于信号处理程序中。同时,由于信号包含状态,因此可以异步方式使用,而不用象条件变量那样要求获取互斥锁。但是,信号的效率不如互斥锁高。缺省情况下,如果有多个线程正在等待信号,则解除阻塞的顺序是不确定的。信号在使用前必须先初始化,但是信号没有属性。

3、初始化信号量

  使用sem_init(3RT)可以将sem所指示的未命名信号变量初始化为value。

  sem_init语法

  int sem_init(sem_t *sem, int pshared, unsigned int value);

  #include

  sem_t sem;

  int pshared;

  int ret;

  int value;

  

  pshared =0;

  value =1;

  ret = sem_init(&sem, pshared, value);

  如果pshared的值为零,则不能在进程之间共享信号。如果pshared的值不为零,则可以在进程之间共享信号。

  注意:

  (1)多个线程决不能初始化同一个信号。

  (2)不得对其他线程正在使用的信号重新初始化。

4、初始化进程内信号量

  pshared为0时,信号只能由该进程内的所有线程使用。

  #include

  sem_t sem;

  int ret;

  int count = 4;

  

  ret = sem_init(&sem, 0, count);

5、初始化进程间信号量

  pshared不为零时,信号可以由其他进程共享。

  #include

  sem_t sem;

  int ret;

  int count = 4;

  

  ret = sem_init(&sem, 1, count);

6、sem_init返回值

  sem_init()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。

  EINVAL

  描述:参数值超过了SEM_VALUE_MAX。

  ENOSPC

  描述:初始化信号所需的资源已经用完。到达信号的SEM_NSEMS_MAX限制。

  ENOSYS

  描述:系统不支持sem_init()函数。

  EPERM

  描述:进程缺少初始化信号所需的适当权限。

7、增加信号

  sem_post语法

  int sem_post(sem_t *sem);

  #include

  sem_t sem;

  int ret;

  ret = sem_post(&sem);

  如果所有线程均基于信号阻塞,则会对其中一个线程解除阻塞。

  sem_post返回值

  sem_post()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

  EINVAL

  描述: sem所指示的地址非法。

8、基于信号计数进行阻塞

  使用sem_wait(3RT)可以阻塞调用线程,直到sem所指示的信号计数大于零为止,之后以原子方式减小计数。

  sem_wait语法

  int sem_wait(sem_t *sem);

  #include

  sem_t sem;

  int ret;

  ret = sem_wait(&sem);

  sem_wait返回值

  sem_wait()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。

  EINVAL

  描述: sem所指示的地址非法。

  EINTR

  描述:此函数已被信号中断。

9、减小信号计数

  使用sem_trywait(3RT)可以在计数大于零时,尝试以原子方式减小sem所指示的信号计数。

  sem_trywait语法

  int sem_trywait(sem_t *sem);

  #include

  sem_t sem;

  int ret;

  ret = sem_trywait(&sem);

  此函数是sem_wait()的非阻塞版本。sem_trywait()在失败时会立即返回。

  sem_trywait返回值

  sem_trywait()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。

  EINVAL

  描述: sem所指示的地址非法。

  EINTR

  描述:此函数已被信号中断。

  EAGAIN

  描述:信号已为锁定状态,因此该信号不能通过sem_trywait()操作立即锁定。

10、销毁信号状态

  使用sem_destroy(3RT)可以销毁与sem所指示的未命名信号相关联的任何状态。

  sem_destroy语法

  int sem_destroy(sem_t *sem);

  #include

  sem_t sem;

  int ret;

  ret = sem_destroy(&sem);

  sem_destroy返回值

  sem_destroy()在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

  EINVAL

描述: sem所指示的地址非法。

例子1:在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;

//从文件1.dat读取数据,每读一次,信号量加1
void ReadData1(void){
 FILE *fp=fopen("1.dat","r");
 while(!feof(fp)){
  fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
  sem_post(&sem);
  ++size;
 }
 fclose(fp);
}

//从文件2.dat读取数据,每读一次,信号量减1
void ReadData2(void){
 FILE *fp=fopen("2.dat","r");
 while(!feof(fp)){
  fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
  sem_post(&sem);
  ++size;
 }
 fclose(fp);
}

//阻塞等待缓冲区有数据,读取数据进行加法运算后,释放空间,继续等待
void HandleData1(void){
 while(1){
  sem_wait(&sem);
  printf("Plus:%d+%d=%dn",stack[size][0],stack[size][1],
  stack[size][0]+stack[size][1]);
  --size;
 }
}

//阻塞等待缓冲区有数据,读取数据进行乘法运算后,释放空间,继续等待

void HandleData2(void){
 while(1){
  sem_wait(&sem);
  printf("Multiply:%d*%d=%dn",stack[size][0],stack[size][1],
  stack[size][0]*stack[size][1]);
  --size;
 }
}

int main(void){
 pthread_t t1,t2,t3,t4;
 sem_init(&sem,0,0);
 pthread_create(&t1,NULL,(void *)HandleData1,NULL);
 pthread_create(&t2,NULL,(void *)HandleData2,NULL);
 pthread_create(&t3,NULL,(void *)ReadData1,NULL);
 pthread_create(&t4,NULL,(void *)ReadData2,NULL);

 pthread_join(t1,NULL);
}

在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem,得到如下的结果:

Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11

  从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题。

例子2:信号量初始值为16,生产者线程每执行一次,信号量加1,并打印;消费者线程每执行一次,信号量减1,并打印。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

void *producter_f(void *arg);
void *consumer_f(void *arg);
sem_t sem;

int running = 1;

int main (void)
{
  pthread_t consumer_t;
  pthread_t producter_t;

  sem_init(&sem, 0, 16);

  pthread_create(&producter_t, NULL, (void*)producter_f, NULL);
  pthread_create(&consumer_t, NULL, (void*)consumer_f, NULL);

  sleep(1);

  pthread_join(consumer_t, NULL);
  pthread_join(producter_t, NULL);

  sem_destroy(&sem);
  return 0;
}

void *producter_f(void *arg)
{
  int semval = 0; //信号量的初始值
while (running)
  {
  usleep(1);

  sem_post(&sem); //信号量+1
  sem_getvalue(&sem, &semval);//得到信号量的值
  printf("pro num:%d\n",semval);
  }
}

void *consumer_f(void *arg)
{
  int semval = 0;
  while (running)
  {
  usleep(1);

  sem_wait(&sem); //信号量-1
  sem_getvalue(&sem, &semval);
  printf("con num:%d\n",semval);
  }
}

例子3:信号量初始化为1,每当 SaveFile 或 ReadFile 线程调用时,先将信号量-1,使之成为0。这样,当另一线程试图对文件内容操作时,即在 sem_wait(&sem) 处阻塞,从而保证同一时刻只有一个线程对文件内容进行操作。

struct mystruct
{
int i;
char ch;
};

sem_t sem;

int main (void)
{
  pthread_t tid1;
  pthread_t tid2;

  sem_init(&sem, 0, 1);

  pthread_create(&tid1, NULL, (void*)SaveFile, NULL);
  pthread_create(&tid2, NULL, (void*)ReadFile, NULL);

  pthread_join(SaveFile, NULL);
  pthread_join(ReadFile, NULL);

  sem_destroy(&sem);
  return 0;
}

//数据保存到指定文件
void *SaveFile(char *FileName, void *source, int size)
{
FILE *fp;
mystruct s;

sem_wait(&sem);
fp = fopen((char*) FileName, "w");


if (fp != NULL)
{
fread(source, sizeof(s), size, fp);
fclose(fp);
}
else
{
fprintf(stderr, "%s saved error\n\r", FileName);
}
sem_post(&sem);
}

//读取数据到指定缓冲区
void *ReadFile(char *FileName, void *source, int size)
{
FILE *fp;

sem_wait(&sem);


memset(source, 0, size);

fp = fopen(FileName, "r");


if (fp != NULL)
{
fread(source, 1, size, fp);
fclose(fp);
}
else
{
fprintf(stderr, "%s read error\n\r", FileName);
}
sem_post(&sem);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值