信号量
互斥锁初值为1,只能有两个值,加锁则-1,解锁则加1。互斥锁唯一、非空闲等待的特性,使得线程由并行执行变为了串行执行,削弱了线程的并发性,假如多个线程需要共享的资源不唯一,比如打印店中的多台电脑连接了多台打印机,每台电脑能通过任意一台打印机打印文件,那么在进行打印任务时,“共享资源”——打印机显然有多个,若此时仍要使用互斥锁来锁定共享资源,需要创建多个互斥锁,且需要使每个线程尝试申请互斥锁,显然比较麻烦。
多线程编程中使用信号量机制解决这一问题。线程中信号量是互斥锁的升级,其初值不再设置为1,而是设置为N。多线程中使用到的信号量与进程通信中讲解的信号量在本质上没有区别。使用信号量实现线程同步时,线程在访问共享资源时会根据操作类型执行P/V操作:若有线程申请访问共享资源,系统会执行P操作使共享资源计数减一;若由线程释放共享资源,系统会执行V操作使共享资源计数加一。
相对互斥锁而言,信号量既能保证同步,防止数据混乱,又能避免影响线程并发性。
信号量的使用也分为四个步骤:
(1)初始化信号量;
(2)阻塞等待信号量;
(3)唤醒阻塞线程;
(4)释放信号量。
针对以上步骤,Linux系统中提供了一组与线程同步机制中信号量操作相关的函数,这些函数都存在于函数库semaphore.h中,下面将对这些函数接口逐一进行讲解。
① sem_init()
sem_init()函数的声明如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
其中参数sem为指向信号量变量的指针。参数pshared用于控制信号量的作用范围,其取值通常为0与非0,当pshared被设置为0时,信号量将会被放在进程中所有线程可见的地址内,由进程中的线程共享;当pshared被设置为非0值时,信号量将会被放置在共享内存区域,由所有进程共享。参数value用于设置信号量sem的初值。
若函数sem_init()执行成功则返回0,否则返回-1,并设置errno。
② sem_wait()
sem_wait()函数的声明如下:
int sem_wait(sem_t *sem);
其中参数sem为指向信号量变量的指针;sem_wait()函数对应P操作,若调用成功,则会使信号量sem的值减一,并返回0;若调用失败,则返回-1,并设置errno。
sem_wait()与互斥锁中的系统调用pthread_mutex_lock()类似,当sem为0,即共享资源耗尽时,再有线程调用该函数申请资源,则该进程会进入阻塞,直至有其它线程释放资源为止。若不希望线程在申请资源时因资源不足进入阻塞状态,可以使用sem_trywait()函数尝试去为线程申请资源,改函数与互斥锁中的pthread_mutex_trylock()类似,若资源申请不成功会立即返回。
③ sem_post()
sem_post()函数的声明如下:
int sem_post(sem_t *sem);
其中参数sem为指向信号量变量的指针;sem_init()函数对应V操作,若调用成功,则会使信号量sem的值加一,并返回0;若调用失败,则返回-1,并设置errno。
④ sem_destroy()
与互斥锁类似,信号量也是一种系统资源,使用完毕之后应主动回收,Linux系统中用于回收信号量的函数为sem_destroy(),其声明如下:
int sem_destroy(sem_t *sem);
sem_destroy()中参数sem为指向信号量变量的指针;若函数调用成功,则会使信号量sem的值加一,并返回0;若调用失败,则返回-1,并设置errno。
除以上几个函数外,线程中另有一个常用的系统调用,即sem_getvalue(),该函数的功能为获取系统中当前信号量的值,其函数声明如下:
int sem_getvalue(sem_t *sem, int *sval);
其中参数sem为指向信号量变量的指针,参数sval为一个传入指针,用于获取信号量的值,在程序中调用该函数后,信号量sem的值会被存储在参数sval中。
当信号量的初值被设置为1时,信号量与互斥锁的功能相同,因此互斥锁也是信号量的一种。下面通过一个案例来展示使用信号量控制线程同步的方式。
案例11:本案例也来实现一个模拟生产者-消费者模型,但对生产者进行限制:若容器已满,生产者不能生产,需等待消费者消费。案例实现如下:
1 #include <stdlib.h>
2 #include <unistd.h>
3 #include <pthread.h>
4 #include <stdio.h>
5 #include <semaphore.h>
6 #define NUM 5
7 int queue[NUM]; //全局数组实现环形队列
8 sem_t blank_number, product_number; //空格子信号量, 产品信号量
9 void *producer(void *arg)
10 {
11 int i = 0;
12 while (1) {
13 sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待
14 queue[i] = rand() % 1000 + 1; //生产一个产品
15 printf("----Produce---%d\n", queue[i]);
16 sem_post(&product_number); //将产品数++
17 i = (i+1) % NUM; //借助下标实现环形
18 sleep(rand()%1);
19 }
20 }
21 void *consumer(void *arg)
22 {
23 int i = 0;
24 while (1) {
25 sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待
26 printf("-Consume---%d %lu\n", queue[i], pthread_self());
27 queue[i] = 0; //消费一个产品
28 sem_post(&blank_number); //消费掉以后,将空格子数++
29 i = (i+1) % NUM;
30 sleep(rand()%1);
31 }
32 }
33 int main(int argc, char *argv[])
34 {
35 pthread_t pid, cid;
36 sem_init(&blank_number, 0, NUM); //初始化空格子信号量为5
37 sem_init(&product_number, 0, 0); //初始化产品数信号量为0
38 pthread_create(&pid, NULL, producer, NULL);
39 pthread_create(&cid, NULL, consumer, NULL);
40 pthread_create(&cid, NULL, consumer, NULL);
41 pthread_join(pid, NULL);
42 pthread_join(cid, NULL);
43 sem_destroy(&blank_number);
44 sem_destroy(&product_number);
45 return 0;
46 }
以上程序中定义了空格子信号量和产品数信号量,使用这两个信号量来控制生产者和消费者的执行:若程序中的队列存满,空格子信号量值为0,此时生产者线程停止生产数据,并向消费者线程发送信号,提醒消费者线程读取数据;若程序中队列为空,产品数信号量为0,此时消费者无法获取数据,便会向生产者线程发送信号,提醒生产者线程生产数据。
编译案例11,执行程序,执行结果如下:
----Produce---510
----Produce---361
----Produce---175
----Produce---454
----Produce---764
-Consume---175 139641890612992
-Consume---454 139641890612992
-Consume---764 139641890612992
-Consume---764 139641880123136
-Consume---510 139641890612992
由程序执行结果可知,案例实现成功。