信号量本身代表一种资源,其本质是一个非负的整数计数器,被用来控制对公共资源的访问。换句话说,信号量的核心内容是信号量的值。其工作原理是:所有对共享资源操作的线程,在访问共享资源之前,都需要先操作信号量的值。操作信号量的值又可以称为 PV
操作, P
操作为申请信号量, V
操作为释放信号量。当申请信号量成功时,信号量的值减1,而释放信号量成功时,信号量的值加1。但是当信号量的值为 0
时,申请信号量时将会阻塞,其值不能减为负数。利用这一特性,即可实现对共享资源访问的控制。
信号量作为一种同步互斥机制,若用于实现互斥时,多线程只需设置一个信号量。若用于实现同步时,则需要设置多个信号量,并通过设置不同的信号量的初始值来实现线程的执行顺序。
本笔记将介绍基于 POSIX
的无名信号量,其信号量的操作与互斥锁类似。
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);点击复制复制失败已复制
sem_init()
函数被用来进行信号量的初始化。参数 sem
表示信号量的标识符。
pshared
参数用来设置信号量的使用环境,其值为 0
,表示信号量用于同一个进程的多个线程之间使用;其值为非 0
,表示信号量用于进程间使用。 value
为重要的参数,表示信号量的初始值。
#include <semaphore.h> int sem_destroy(sem_t *sem);点击复制复制失败已复制
sem_destroy()
函数被用来摧毁信号量,参数 sem
表示信号量的标志符。
#include <semaphore.h> int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem); int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);点击复制复制失败已复制
sem_wait()
函数用来执行申请信号量的操作,当申请信号量成功时,信号量的值减1,当信号量的值为 0
时,此操作将会阻塞,直到其他线程执行释放信号量。
sem_trywait()
函数与 sem_wait()
函数类似,唯一的区别在于 sem_trywait()
函数不会阻塞,当信号量为 0
时,函数直接返回错误码EAGAIN
。
sem_timedwait()
函数同样,多了参数 abs_timeout
,用来设置时间限制,如果在该时间内,信号量仍然不能申请,那么该函数不会一直阻塞,而是返回错误码 ETIMEOUT
。
#include <semaphore.h> int sem_post(sem_t *sem);点击复制复制失败已复制
sem_post()
函数用来执行释放信号量的操作,当释放信号量成功时,信号量的值加
1。
#include <semaphore.h> int sem_getvalue(sem_t *sem, int *sval);点击复制复制失败已复制
sem_getvalue()
函数用于获得当前信号量的值,并保存在参数 sval
中。
信号量可以应用的场合很多,下例展示其使用方法:
#include <pthread.h> #include <stdio.h> #include <semaphore.h> #include <string.h> #define N 32 #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) char buf[N] = ""; void *thread1_handler(void *arg) { while (1) { fgets(buf,N,stdin); buf[strlen(buf)-1] = '\0'; } pthread_exit("thread1...exit"); } void *thread2_handler(void *arg) { while (1) { printf("buf:%s\n",buf); sleep(1); } pthread_exit("thread2...exit"); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2; if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } pthread_join(thread1, NULL); pthread_join(thread2, NULL); return 0; }点击复制复制失败已复制
编译并运行,结果如下:
$ gcc main.c && ./a.out buf: buf: buf: buf:sssdfsdfad buf:sssdfsdfad buf:sssdfsdfad buf:sssdfsdfad点击复制复制失败已复制
可以看出,线程 1
中 fgets()
是一个阻塞函数,比较特殊。但是线程 2
在共享数据并不确定的情况下,轮询读取共享的数组,当终端输入内容后(线程1读取输入内容),线程 2
则读取输入的内容并将内容一直打印输出,这样的结果并不是程序的本意。
程序设计的目的是让线程 1
可以写数据,线程 2
读数据,并保证数据的实时、有效,因此在这里可以选择引入两个信号量,实现同步的操作,使线程可以按照一定的顺序实现写入与读取,如下所示:
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <string.h> #define N 32 #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) char buf[N] = ""; sem_t sem1, sem2; void *thread1_handler(void *arg) { while (1) { sem_wait(&sem2); fgets(buf, N, stdin); buf[strlen(buf) - 1] = '\0'; sem_post(&sem1); } pthread_exit("thread1...exit"); } void *thread2_handler(void *arg) { while (1) { sem_wait(&sem1); printf("buf:%s\n", buf); sleep(1); sem_post(&sem2); } pthread_exit("thread2...exit"); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2; if (sem_init(&sem1, 0, 0) < 0) { errlog("sem_init error"); } if (sem_init(&sem2, 0, 1) < 0) { errlog("sem_init error"); } if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } pthread_join(thread1, NULL); pthread_join(thread2, NULL); sem_destroy(&sem1); sem_destroy(&sem2); return 0; }点击复制复制失败已复制
编译并运行,结果如下:
$ gcc main.c && ./a.out hello buf:hello world buf:world点击复制复制失败已复制
线程 2
一开始就不会直接输出,而是阻塞等待;当线程 1
对共享资源写操作完成之后,可以进行读操作并输入,之后可以继续写入并读取,后续维持此状态。