1、 概述
在linux中,线程就相当于一个轻量级的进程,它常常被用来完成某种特定功能的事情。假如一个进程创建了多个线程,这些线程要一起配合完成一件更大的事情,这个时候就需要用到线程同步机制了。在Linux中通常用信号量实现线程间的同步。
这种情形可以用现实生活中来举例子,比如甲乙两个人用双人手拉锯锯木头,甲拉一下然后乙拉一下,必须这样才能配合把木头锯断。在这个情形之下,甲拉完一下之后必须等乙拉完才能再次拉,乙也是如此,它们之间的信号量值最大为1。
又或者甲乙丙三个人种树,甲负责挖洞,乙负责放树苗,丙负责填洞,那流程是“挖洞->放树苗->填洞”, “挖洞->放树苗->填洞”...。甲的工作不用受乙和丙的影响,他可以在乙来不及放树苗的情况下挖很多洞,而乙放树苗只要等甲挖好洞了就可以放,他不用管丙填了多少个洞,如果乙将所有的洞都放好了树苗,那么乙就必须等待甲挖好下一个洞才能放树苗,同样的如果丙将所有放了树苗的坑都填了,那么他必须等乙放好下一棵树苗才能填洞,而不关心甲挖了多少个洞。这种情况下甲每挖一个洞,甲和乙之间的信号量的值加一,乙每放一棵树苗,甲和乙之间的信号量的值减一,如果信号量的值为0,那么乙就得等。乙和丙也是如此,但是“甲—乙”和“乙—丙”之间用的信号量不是同一个。
信号量分为有名信号量和无名信号量,实际上线程间同步用的是无名信号量,进程间同步才用有名信号量(也可以用无名信号量进行进程间同步)。
使用信号量需要包含头文件semaphore.h,且编译时需要链接库-lrt或者-lpthread。
2、函数介绍
2.1 初始化信号量
2.1.1 sem_init
函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
参数[out]:sem:初始化的信号量。
参数[in]:pshared:信号量共享的范围。
0:线程间使用。
非0:进程间使用。
参数[in]:value:信号量初值。
返回:成功返回0,失败返回-1。
2.2 获取信号量
2.2.1 sem_wait
函数原型:int sem_wait(sem_t *sem);
功能:等待信号量。如果信号量的值大于0,那么该函数立即返回,信号量的值减1,如果信号量的值等于0,那么阻塞等待直到信号量的的值大于1,然后信号量的值减1。
参数[in]:sem:从sem_init函数得到的信号量。
返回:成功返回0,失败返回-1。
2.2.2 sem_trywait
函数原型:int sem_trywait(sem_t *sem);
功能:尝试等待信号量。该函数会立即返回,如果信号量的值大于0,那么信号量的值减1,函数返回0,否则函数返回-1,并且将errno置为EAGAIN,其值为11,表示Try again,造成错误的原因是“资源暂时不可用”。
参数[in]:sem:从sem_init函数得到的信号量。
返回:成功返回0,失败返回-1。
2.2.3 sem_timedwait
函数原型:int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:在传入的时间内阻塞等待信号量。
参数[in]:sem:从sem_init函数得到的信号量。
参数[in]:abs_timeout:阻塞等待的系统实时时间的时间点。struct timespec结构定义如下:
struct timespec
{
time_t tv_sec; /* 秒*/
long tv_nsec; /* 纳秒*/
};
特别注意该结构表示的不是延时等待的时间,而是系统的实时时间。我开始以为比如等待3秒,那么将tv_sec设置为3,tv_nsec设置为0即可,这样带来的结果是函数立刻返回了。传入的值实际上是某个时刻,可以认为它等到这个时刻如果还没有信号量被释放他就不等了。想要确定此时的时刻是多少,可以通过clock_gettime函数获取,该函数的函数原型为:int clock_gettime(clockid_t clk_id, struct timespec* tp);
调用clock_gettime函数需包含time.h,编译时需要链接-lrt
clk_id有下列几种选择:
CLOCK_REALTIME:系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,中间时刻如果系统时间被用户改成其他,则对应的时间相应改变
CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码系统CPU花费的时间
CLOCK_THREAD_CPUTIME_ID:本线程到当前代码系统CPU花费的时间
由于sem_timedwait函数等待的是系统实时时间,因此超时等待三秒的代码可以这样写:
clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3; ret = sem_timedwait(&sem, &ts);
在调用sem_timedwait时,如果有信号量,那么函数立即返回0。
如果没有信号量释放,并且系统时间已经过了指定的时间,那么函数立即返回-1,并且将errno置为ETIMEDOUT,其值为110,表示超时。
如果没有信号量释放,并且系统时间还没过指定的时间,如果在时间到之前有信号量释放,函数返回0,如果时间到了还没有信号量释放,那么函数返回-1,并且置errno为ETIMEDOUT。
这里有一个类似于bug的东西,由于sem_timedwait函数是等待系统时间到达某个点,如果在等待期间系统时间被改变了,等待的时间点却不会变。这就像你和女朋友约好了晚上8点见面,实际上指的是你手表上的晚上8点,你只要保证你的手表上的时间不会达到8点,那么就不会超时。
返回:成功返回0,失败返回-1。
2.3 释放信号量
2.3.1 sem_post
函数原型:int sem_post(sem_t *sem);
功能:释放信号量,每调用一次sem_post,信号量的值加1。
参数:sem:信号量
返回:成功返回0,失败返回-1。失败的情况有2种,第一是传入的信号量无效,那么errno被置为EINVAL,第二种是信号量的值将要超过可达到的最大值,那么errno被设置为EOVERFLOW。这个最大值就是int类型的最大值2147483647(2^31 - 1)。
2.4 获取信号量的值
2.4.1 sem_getvalue
函数原型:int sem_getvalue(sem_t *sem, int *sval);
功能:获取信号量的值。
参数[in]:sem:信号量
参数[out]:sval:用于保存信号量的值
返回:成功返回0,失败返回-1。
2.5 销毁信号量
2.5.1 int sem_destroy
函数原型:int sem_destroy(sem_t *sem);
功能:销毁信号量。
参数[in]:sem:信号量
返回:成功返回0,失败返回-1。
3、 测试程序
3.1 测试程序1
1 /** 2 * filename: sem.c 3 * author: Suzkfly 4 * date: 2021-01-27 5 * platform: Ubuntu 6 * 该例程测试了sem_wait的用法 7 * 编译时加-lpthread 8 */ 9 #include <stdio.h> 10 #include <pthread.h> 11 #include <semaphore.h> 12 13 sem_t g_sem; /* 定义信号量 */ 14 15 void *pthread_func1(void *p_arg) 16 { 17 int value = 0; 18 19 sem_getvalue(&g_sem, &value); /* 获取信号量的值 */ 20 printf("value = %d\n", value); 21 printf("%s sem_wait...\n", __func__); 22 sem_wait(&g_sem); 23 printf("%s sem_wait succeed\n", __func__); 24 } 25 26 void *pthread_func2(void *p_arg) 27 { 28 int value = 0; 29 30 sleep(1); /* 让pthread_func1先获取到信号量 */ 31 sem_getvalue(&g_sem, &value); 32 printf("value = %d\n", value); 33 printf("%s sem_wait...\n", __func__); 34 sem_wait(&g_sem); 35 printf("%s sem_wait succeed\n", __func__); /* 这句打印不出来 */ 36 } 37 38 int main(int argc, const char *argv[]) 39 { 40 pthread_t pthread; 41 pthread_t pthread2; 42 int ret; 43 44 ret = sem_init(&g_sem, 0, 1); /* 初始化信号量值为1 */ 45 if (ret == -1) { 46 printf("sem_init failed\n"); 47 return 0; 48 } 49 50 pthread_create(&pthread, NULL, pthread_func1, NULL); /* 创建线程 */ 51 pthread_create(&pthread2, NULL, pthread_func2, NULL); /* 创建线程 */ 52 53 pthread_join(pthread, NULL); /* 阻塞等待回收线程资源 */ 54 pthread_join(pthread2, NULL); /* 阻塞等待回收线程资源 */ 55 56 return 0; 57 }
测试结果:
代码分析:
第44行将信号量的值初始化为1,表明在没有调用sem_post的情况下只能有一次sem_wait成功,在第30行让线程2睡眠1秒,目的是确保线程1先得到信号量,线程1在sem_wait之前先获取信号量的值,其值为1,那么在sem_wait之后信号量的值变为0,那么线程2调用sem_wait将一直阻塞,不会执行后面的语句。
3.2 测试程序2
1 /** 2 * filename: sem.c 3 * author: Suzkfly 4 * date: 2021-01-27 5 * platform: Ubuntu 6 * 该例程测试了sem_wait的用法,实现拉锯效果 7 * 编译时加-lpthread 8 */ 9 #include <stdio.h> 10 #include <pthread.h> 11 #include <semaphore.h> 12 13 sem_t g_sem[2]; /* 定义信号量 */ 14 15 void *pthread_func1(void *p_arg) 16 { 17 while (1) { 18 sem_wait(&g_sem[0]); 19 printf("1\n"); 20 sem_post(&g_sem[1]); 21 } 22 } 23 24 void *pthread_func2(void *p_arg) 25 { 26 while (1) { 27 sem_wait(&g_sem[1]); 28 printf("2\n"); 29 sem_post(&g_sem[0]); 30 } 31 } 32 33 int main(int argc, const char *argv[]) 34 { 35 pthread_t pthread; 36 pthread_t pthread2; 37 38 sem_init(&g_sem[0], 0, 1); /* 初始化信号量值为0 */ 39 sem_init(&g_sem[1], 0, 0); /* 初始化信号量值为0 */ 40 41 pthread_create(&pthread, NULL, pthread_func1, NULL); /* 创建线程 */ 42 pthread_create(&pthread2, NULL, pthread_func2, NULL); /* 创建线程 */ 43 44 pthread_join(pthread, NULL); /* 阻塞等待回收线程资源 */ 45 pthread_join(pthread2, NULL); /* 阻塞等待回收线程资源 */ 46 47 return 0; 48 }
测试结果:
代码分析:
该程序初始化了2个信号量,但给g_sem[0]的值为1,给g_sem[1]的值为0,在两个线程中互相给对方释放信号量,这样就能不断的打印121212...,就实现了拉锯的效果。
3.3 测试程序3
1 /** 2 * filename: sem.c 3 * author: Suzkfly 4 * date: 2021-01-27 5 * platform: Ubuntu 6 * 该例程测试了sem_wait的用法,实现“挖洞->放树苗->填洞” 7 * 编译时加-lpthread 8 */ 9 #include <stdio.h> 10 #include <pthread.h> 11 #include <semaphore.h> 12 13 sem_t g_sem[2]; /* 定义信号量 */ 14 15 void *pthread_func1(void *p_arg) 16 { 17 int i = 5; 18 19 while (i--) { 20 printf("Dig\n"); /* 挖洞 */ 21 //usleep(100); /* 如果觉得挖洞挖的太快就去掉本行注释 */ 22 sem_post(&g_sem[0]); 23 } 24 } 25 26 void *pthread_func2(void *p_arg) 27 { 28 int i = 5; 29 30 while (i--) { 31 sem_wait(&g_sem[0]); 32 printf("Plant\n"); /* 种树 */ 33 sem_post(&g_sem[1]); 34 } 35 } 36 37 void *pthread_func3(void *p_arg) 38 { 39 int i = 5; 40 41 while (i--) { 42 sem_wait(&g_sem[1]); 43 printf("Fill\n"); /* 填洞 */ 44 } 45 } 46 47 int main(int argc, const char *argv[]) 48 { 49 pthread_t pthread; 50 pthread_t pthread2; 51 pthread_t pthread3; 52 53 sem_init(&g_sem[0], 0, 0); /* 初始化信号量值为0 */ 54 sem_init(&g_sem[1], 0, 0); /* 初始化信号量值为0 */ 55 56 pthread_create(&pthread, NULL, pthread_func1, NULL); /* 创建线程 */ 57 pthread_create(&pthread2, NULL, pthread_func2, NULL); /* 创建线程 */ 58 pthread_create(&pthread3, NULL, pthread_func3, NULL); /* 创建线程 */ 59 60 pthread_join(pthread, NULL); /* 阻塞等待回收线程资源 */ 61 pthread_join(pthread2, NULL); /* 阻塞等待回收线程资源 */ 62 pthread_join(pthread3, NULL); /* 阻塞等待回收线程资源 */ 63 64 return 0; 65 }
测试结果:
代码分析:
本程序初始化的信号量都为0,线程1不需要等待信号量,但是它每执行一遍就给线程2释放一次信号量,线程2得到信号量后给线程3释放一次信号量,该程序的执行结果不一定如上图那样并排下来,它只是保证了“种树前必须有挖好的坑,填坑前必须有种好的树”。
3.4 测试程序4
1 /** 2 * filename: sem.c 3 * author: Suzkfly 4 * date: 2021-01-27 5 * platform: Ubuntu 6 * 该例程测试了sem_trywait的用法 7 * 编译时加-lpthread 8 */ 9 #include <stdio.h> 10 #include <pthread.h> 11 #include <semaphore.h> 12 #include <errno.h> 13 14 sem_t g_sem; /* 定义信号量 */ 15 16 void *pthread_func(void *p_arg) 17 { 18 int ret = 0; 19 int value = 0; 20 21 while (1) { 22 sem_getvalue(&g_sem, &value); 23 printf("value = %d\n", value); 24 printf("sem_trywait...\n"); 25 ret = sem_trywait(&g_sem); 26 printf("ret = %d\n", ret); 27 printf("errno = %d\n", errno); 28 sleep(1); 29 } 30 } 31 32 int main(int argc, const char *argv[]) 33 { 34 pthread_t pthread; 35 36 sem_init(&g_sem, 0, 1); /* 初始化信号量值为1 */ 37 38 pthread_create(&pthread, NULL, pthread_func, NULL); /* 创建线程 */ 39 40 pthread_join(pthread, NULL); /* 阻塞等待回收线程资源 */ 41 42 return 0; 43 }
测试结果:
代码分析:
第36行将信号量的初值设为1,在运行程序的时候第一次获取信号量的值为1,sem_trywait返回0,errno的值也为0,之后再获取信号量的时候value就一直为0了,并且sem_trywait返回-1,errno的值为11,表示“资源暂时不可用”。
3.5 测试程序5
1 /** 2 * filename: sem.c 3 * author: Suzkfly 4 * date: 2021-01-27 5 * platform: Ubuntu 6 * 该例程测试了sem_timedwait的用法 7 * 编译时加-lrt 8 */ 9 #include <stdio.h> 10 #include <pthread.h> 11 #include <semaphore.h> 12 #include <errno.h> 13 #include <time.h> 14 15 sem_t g_sem; /* 定义信号量 */ 16 17 void *pthread_func(void *p_arg) 18 { 19 int ret; 20 struct timespec ts; 21 22 clock_gettime(CLOCK_REALTIME, &ts); 23 ts.tv_sec += 5; 24 ret = sem_timedwait(&g_sem, &ts); 25 printf("ret = %d\n", ret); 26 printf("errno = %d\n", errno); 27 } 28 29 int main(int argc, const char *argv[]) 30 { 31 pthread_t pthread; 32 33 sem_init(&g_sem, 0, 0); /* 初始化信号量值为0 */ 34 35 pthread_create(&pthread, NULL, pthread_func, NULL); /* 创建线程 */ 36 37 while (1) { 38 sleep(1); 39 printf("running...\n"); 40 } 41 pthread_join(pthread, NULL); /* 阻塞等待回收线程资源 */ 42 43 return 0; 44 }
测试结果:
由于初始信号量给的是0,所以sem_timedwait函数在等待了5秒之后才返回。需要注意的是,由于使用了clock_gettime函数,因此编译时需要连接库-lrt。