12.5 同步
在上一节中,看到两个线程同时执行的情况,但采用的在它们之间切换的方法是非常笨拙并且没有效率的.幸运的是,专门有一组设计好的函数提供了更好的控制线程执行和访问代码临界区域的方法.在本节学习 两种基本的方法.一种是信号量,它的作用如同看守一段代码的看门人;另一种是互斥量,它的作用如同保护代码段的一个互斥设备.
这两种方法很类似,事实上,它们可以互相通过对方来实现.但在实际应用中,对于一些情况,可能使用信号量或互斥量中的一个更符合问题的语义,并且效果更好.例如,如果想控制任一时刻只能有一个线程可以访问一些共享内存,使用互斥量就要自然得多.但在控制对一组相同对象的访问时--比如从5条可用的电话线中分配一条给某个线程的情况,就更适合使用计数信号量.具体选择哪种方法取决于个人偏好和响应的程序机制.
12.5.1 用信号量进行同步
有两组接口函数用于信号量,一组取自POSIX的实时扩展,用于线程.另一组被成为系统V信号量,用于进程的同步.这两组接口函数虽然很相近,但并不保证它们之间可以互换,而且它们使用的函数调用也各不相同.荷兰计算机科学家 Dijkstra首先提出了信号量的概念. 信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此.这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都将一次进行.但如果是普通变量,来自同一程序中的不同线程的冲突操作所导致的结果将是不确定的.
在本节中,将介绍一种最简单的信号量--二进制信号量,它只有0和1两种取值.还有一种更通用的信号量--计数信号量,它可以有更大的取值范围.信号量一般常用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作,就要使用二进制信号量.有时,希望可以允许有限数目的线程执行一段指定的代码,这就需要用到计数信号量.
信号量函数的名字都以sem_开头(semaphore),而不像大多数线程函数那样以pthread_开头.线程中使用的基本信号量函数有4个,它们都非常简单.
信号量通过sem_init函数创建,它的定义如下所示:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
这个函数初始化由sem指向的信号量对象,设置它的共享选项,并给它一个初始的整数值.
pshared参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享.
接下来的两个函数控制信号量的值,它们的定义如下所示:
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
这两个函数都以指针为参数,该指针指向的对象是由sem_init调用初始化的信号量.
sem_post函数的作用是以原子操作的方式给信号量的值加1.所谓原子操作是指,如果两个线程企图同时给一个信号量加1,它们之间不会互相干扰,而不像如果两个程序同时对同一个文件进行读取,增加,写入操作时可能会引起冲突.信号量的值总是会被正确地加1,因为有两个线程试图改变它.
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到这个信号量有个非零值才会开始减法操作.因此,如果对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1.如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其他线程增加了该信号量的值使其不再是0为止.如果两个线程同时在sem_wait调用上等待同一个信号量变为非零,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待.信号量的这种"在单个函数中就能原子化进行测试和设置"的能力使其变得非常有价值.
最后一个信号量函数是 sem_destroy.这个函数的作用是, 用完信号量后对它进行清理.它的定义如下:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
这个函数以一个信号量指针为参数,并清理该信号量所拥有的资源.如果企图清理的信号量正在被一些线程等待,就会收到一个错误.
编写程序thread3.c,这个程序基于thread1.c.
/*************************************************************************
> File Name: thread3.c
> Description: thread3.c程序演示了利用信号量实现输入输出同步,原来的线程和新线程共享同一个work_area数组
> Author: Liubingbing
> Created Time: 2015年07月05日 星期日 18时23分44秒
> Other: thread3.c程序通过在主线程中调用sem_post和新线程中调用sem_wait实现同步
************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_function(void *arg);
sem_t bin_sem;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int main(){
int res;
pthread_t a_thread;
void *thread_result;
/* sem_init函数创建信号量
* 第一个参数为指向信号量的指针
* 第二个参数一般为0,这个信号量是当前进程的局部信号量
* 第三个参数为信号量的初始化值,设置bin_sem为0,则线程函数thread_function启动时,sem_wait函数调用就会阻塞并等待信号量变为非零值 */
res = sem_init(&bin_sem, 0, 0);
if (res != 0) {
perror("Semaphore initialization failed");
exit(EXIT_FAILURE);
}
/* pthread_create创建一个新线程,创建成功返回0
* a_thread中写入一个用来引用新线程的标识符
* thread_function是新线程将要启动执行的函数
* NULL是传递给函数的参数 */
res = pthread_create(&a_thread, NULL, thread_function, NULL);
if (res != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
printf("Input some text. Enter 'end' to finish\n");
/* strncmp比较字符串"end"和work_area的前3个字符,如果相等,则返回0 */
while (strncmp("end", work_area, 3) != 0) {
/* fgets从文件流stdin(键盘)读取字符串(最多WORK_SIZE-1个)到字符数组work_area中 */
fgets(work_area, WORK_SIZE, stdin);
/* sem_post函数以原子操作的方式给信号量bin_sem的值加1 */
sem_post(&bin_sem);
}
printf("\nWaiting for thread to finish...\n");
/* pthread_join函数等待线程的执行
* a_thread中的标识符为要等待的线程
* thread_result指向等待线程的返回值 */
res = pthread_join(a_thread, &thread_result);
if (res != 0) {
perror("Thread join failed");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
/* sem_destroy函数对使用完的信号量进行清理*/
sem_destroy(&bin_sem);
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg){
/* sem_wait函数以原子操作的方式将信号量bin_sem的值加1,但它会等待直到信号量有个非零值时才会开始减法操作 */
sem_wait(&bin_sem);
while (strncmp("end", work_area, 3) != 0) {
printf("You input %d characters\n", strlen(work_area) - 1);
sem_wait(&bin_sem);
}
pthread_exit(NULL);
}
第一个重要的改动是包含了头文件semaphore.h,使用它可以访问信号量函数,然后定义一个信号量和几个变量,并在创建新线程之前对信号量进行初始化.
在main函数中,启动新线程后,从键盘读取一些文本并把它们放到工作区work_area数组中,然后调用sem_post增加信号量的值.
在新线程中,等待信号量,然后统计来自输入的字符个数.
设置信号量的同时,等待键盘的输入,当输入到达时,释放信号量,允许第二个线程在第一个线程再次读取键盘输入之前统计出输入字符的个数.
这两个线程共享同一个work_area数组.
在初始化信号量时,把它设置为0,这样,在线程函数启动时,sem_wait函数调用就会阻塞并等待信号量变为非零值.
在主线程中,等待直到有文本输入,然后调用sem_post增加信号量的值,这将立刻令另一个线程从sem_wait的等待中返回并执行.在统计完字符个数后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值为止.
如果将main函数中的读数据循环修改为:
while (strncmp("end", work_area, 3) != 0) {
if (strncmp(work_area, "FAST", 4) == 0) {
sem_post(&bin_sem);
strcpy(work_area, "Wheeeeeee....");
} else {
fgets(work_area, WORK_SIZE, stdin);
}
sem_post(&bin_sem);
}
编译运行后,输入FAST时,则会出现问题.
问题在于,程序依赖其接收文本输入的时间要足够长,这样另一个线程才有时间在主线程还未准备好给它更多的单词去统计之前统计出工作区的字符的个数.
当试图连续快速给它两组不同的单词去统计时(键盘输入的FAST和程序自动提供的Wheeeeee....),第二个线程就没有时间去执行(统计FAST), 因为主线程中的程序已经紧接着赋给work_area新的字符串了.(输入FAST之后,sem_post(&bin_sem),赋新字符串,接着sem_post(&bin_sem),此时work_area不为FAST,因此else等待输入,bin_sem增加两次,但是sem_wait语句在printf语句之后,因此输出三次printf,如下所示:
这个例子显示:在多线程中,需要对时序考虑的非常仔细.为了解决上面程序中的问题,可以再增加一个信号量,让主线程等到统计线程完成字符个数的统计后再执行,但更简单的一种方式是使用互斥量.