信号量:它允许多个线程在同一时刻访问同一资源(临界区),但是需要限制在同一时刻访问此资源的最大线程数目
信号量常用的API
信号量创建
int sem_init(sem_t *sem, int pshared, unsigned int value);,
sem是要初始化的信号量,为0时表示只能在当前进程的多个线程之间共享,pshared表示此信号量是在进程间共享还是线程间共享,value是信号量的初始值
信号量销毁
int sem_destroy(sem_t *sem);
等待信号量
int sem_wait(sem_t *sem);
等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1
释放信号量
int sem_post(sem_t *sem);
释放信号量,让信号量的值加1。相当于V操作
使用信号量同步线程
下面以一个简单的多线程程序来说明如何使用信号量进行线程同步。在主线程中,我们创建子线程,并把数组msg作为参数传递给子线程,然后主线程等待直到有文本输入,然后调用sem_post来增加信号量的值,这样就会立刻使子线程从sem_wait的等待中返回并开始执行。线程函数在把字符串的小写字母变成大写并统计输入的字符数量之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值。
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//线程函数
void *thread_func(void *msg);
sem_t sem;//信号量
#define MSG_SIZE 512
int main()
{
int res = -1;
pthread_t thread;
void *thread_result = NULL;
char msg[MSG_SIZE];
//初始化信号量,其初值为0
res = sem_init(&sem, 0, 0);
if(res == -1)
{
perror("semaphore intitialization failed\n");
exit(EXIT_FAILURE);
}
//创建线程,并把msg作为线程函数的参数
res = pthread_create(&thread, NULL, thread_func, msg);
if(res != 0)
{
perror("pthread_create failed\n");
exit(EXIT_FAILURE);
}
//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
printf("Input some text. Enter 'end'to finish...\n");
while(strcmp("end\n", msg) != 0)
{
fgets(msg, MSG_SIZE, stdin);
//把信号量加1
sem_post(&sem);
}
printf("Waiting for thread to finish...\n");
//等待子线程结束
res = pthread_join(thread, &thread_result);
if(res != 0)
{
perror("pthread_join failed\n");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
//清理信号量
sem_destroy(&sem);
exit(EXIT_SUCCESS);
}
void* thread_func(void *msg)
{
//把信号量减1
sem_wait(&sem);
char *ptr = msg;
while(strcmp("end\n", msg) != 0)
{
int i = 0;
//把小写字母变成大写
for(; ptr[i] != '\0'; ++i)
{
if(ptr[i] >= 'a' && ptr[i] <= 'z')
{
ptr[i] -= 'a' - 'A';
}
}
printf("You input %d characters\n", i-1);
printf("To Uppercase: %s\n", ptr);
//把信号量减1
sem_wait(&sem);
}
//退出线程
pthread_exit(NULL);
}
运行结果如下:
从运行的结果来看,这个程序的确是同时在运行两个线程,一个控制输入,另一个控制处理统计和输出。
对于上面信号量例子的思考
分析此信号量同步程序的缺陷
但是这个程序有一点点的小问题,就是这个程序依赖接收文本输入的时间足够长,这样子线程才有足够的时间在主线程还未准备好给它更多的单词去处理和统计之前处理和统计出工作区中字符的个数。所以当我们连续快速地给它两组不同的单词去统计时,子线程就没有足够的时间支执行,但是信号量已被增加不止一次,所以字符统计线程(子线程)就会反复处理和统计字符数目,并减少信号量的值,直到它再次变成0为止。
为了更加清楚地说明上面所说的情况,修改主线程的while循环中的代码,如下:
printf("Input some text. Enter 'end'to finish...\n");
while(strcmp("end\n", msg) != 0)
{
if(strncmp("TEST", msg, 4) == 0)
{
strcpy(msg, "copy_data\n");
sem_post(&sem);
}
fgets(msg, MSG_SIZE, stdin);
//把信号量加1
sem_post(&sem);
}
重新编译程序,此时运行结果如下:
当我们输入TEST时,主线程向子线程提供了两个输入,一个是来自键盘的输入,一个来自主线程复数据到msg中,然后从运行结果可以看出,运行出现了异常,没有处理和统计从键盘输入TEST的字符串而却对复制的数据作了两次处理。原因如上面所述。
解决此缺陷的方法
解决方法有两个,一个就是再增加一个信号量,让主线程等到子线程处理统计完成之后再继续执行;另一个方法就是使用互斥量。
下面给出用增加一个信号量的方法来解决该问题的代码,源文件名为semthread2.c,源代码如下:
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//线程函数
void *thread_func(void *msg);
sem_t sem;//信号量
sem_t sem_add;//增加的信号量
#define MSG_SIZE 512
int main()
{
int res = -1;
pthread_t thread;
void *thread_result = NULL;
char msg[MSG_SIZE];
//初始化信号量,初始值为0
res = sem_init(&sem, 0, 0);
if(res == -1)
{
perror("semaphore intitialization failed\n");
exit(EXIT_FAILURE);
}
//初始化信号量,初始值为1
res = sem_init(&sem_add, 0, 1);
if(res == -1)
{
perror("semaphore intitialization failed\n");
exit(EXIT_FAILURE);
}
//创建线程,并把msg作为线程函数的参数
res = pthread_create(&thread, NULL, thread_func, msg);
if(res != 0)
{
perror("pthread_create failed\n");
exit(EXIT_FAILURE);
}
//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
printf("Input some text. Enter 'end'to finish...\n");
sem_wait(&sem_add);
while(strcmp("end\n", msg) != 0)
{
if(strncmp("TEST", msg, 4) == 0)
{
strcpy(msg, "copy_data\n");
sem_post(&sem);
//把sem_add的值减1,即等待子线程处理完成
sem_wait(&sem_add);
}
fgets(msg, MSG_SIZE, stdin);
//把信号量加1
sem_post(&sem);
//把sem_add的值减1,即等待子线程处理完成
sem_wait(&sem_add);
}
printf("Waiting for thread to finish...\n");
//等待子线程结束
res = pthread_join(thread, &thread_result);
if(res != 0)
{
perror("pthread_join failed\n");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
//清理信号量
sem_destroy(&sem);
sem_destroy(&sem_add);
exit(EXIT_SUCCESS);
}
void* thread_func(void *msg)
{
char *ptr = msg;
//把信号量减1
sem_wait(&sem);
while(strcmp("end\n", msg) != 0)
{
int i = 0;
//把小写字母变成大写
for(; ptr[i] != '\0'; ++i)
{
if(ptr[i] >= 'a' && ptr[i] <= 'z')
{
ptr[i] -= 'a' - 'A';
}
}
printf("You input %d characters\n", i-1);
printf("To Uppercase: %s\n", ptr);
//把信号量加1,表明子线程处理完成
sem_post(&sem_add);
//把信号量减1
sem_wait(&sem);
}
sem_post(&sem_add);
//退出线程
pthread_exit(NULL);
}
其运行结果如下:
分析:这里我们多使用了一个信号量sem_add,并把它的初值赋为1,在主线程在使用sem_wait来等待子线程处理完全,由于它的初值为1,所以主线程第一次调用sem_wait总是立即返回,而第二次调用则需要等待子线程处理完成之后。而在子线程中,若处理完成就会马上使用sem_post来增加信号量的值,使主线程中的sem_wait马上返回并执行紧接下面的代码。从运行结果来看,运行终于正常了。注意,在线程函数中,信号量sem和sem_add使用sem_wait和sem_post函数的次序,它们的次序不能错乱,否则在输入end时,可能运行不正常,子线程不能正常退出,从而导致程序不能退出。