信号量
信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此。这意味着若果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。但如果是普通变量,来自同一程序中的不同线程的冲突操作导致的结果将是不确定的。
信号量--二进制信号量,它只有 0 和 1 两种取值。还有一种更通用的信号量--计数信号量,它可以有更大的取值范围。信号量一般常用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作。就要使用二进制信号量。由于计数信号量并不常用。所以我们在这里不对它进行深入介绍,实际上它仅仅是二进制信号量的一种逻辑扩展,两者实际调用的函数都一样。
信号量通过 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 ,它们之间不会互相干扰,而不像如果两个程序同时对一个文件进行读取、增加、写入操作时可能会引起冲突。信号量的值总是会被正确的加 2,因为有两个线程试图改变它。
sem_wait 函数以原子操作的方式将信号量减 1,但它会等待直到信号量有个非零值才会开始减法操作。因此,若果对值为 2 的信号量调用 sem_wait,线程将继续执行,但信号量的值会减到 1.如果对值为 0 的信号量调用 sem_wait ,这个函数就会等待,直到有其它线程增加了该信号量的值使其不再为 0 为止。如果两个线程同时在 sem_wait 调用上等待同一个信号量变为非零值,那么该信号量被被第三个线程增加 1 时,只有其中一个等待线程将开始对信号量减一,然后继续执行,另外一个线程还将继续等待。信号量的这种“在单个函数中就能原子化地进行测试和设置”的能力使其变得非常有价值。
还有另外一个信号量函数 sem_trywait ,它是 sem_wait 的非阻塞版本。
sem_destroy ,这个函数的作用是,用完信号量后对它进行清理。它的定义如下:
#include <semaphore.h>
int sem_destroy (sem_t *sem);
这个函数也以一个信号量指针为参数,并清理该信号量拥有的所有资源。如果企图清理的信号量正被一些线程等待,就会收到一个错误,成功时返回 0.
#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;
res = sem_init(&bin_sem, 0, 0);
if (res != 0) {
perror("Semaphore initialization failed");
exit(EXIT_FAILURE);
}
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");
while(strncmp("end", work_area, 3) != 0) {
fgets(work_area, WORK_SIZE, stdin);
sem_post(&bin_sem);
}
printf("\nWaiting for thread to finish...\n");
res = pthread_join(a_thread, &thread_result);
if (res != 0) {
perror("Thread join failed");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
sem_destroy(&bin_sem);
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
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);
}
运行程序:
$./thread3
Input some text. Enter ‘end’ to finish
The Wasp Factory
You input 16 characters
Iain Banks
You input 10 characters
end
Waiting for thread to finish...
Thread joined
实验解析:
初始化信号量时,我们把它的值设置为 0。这样,在线程函数启动时,sem_wait 函数调用就会阻塞并等待信号量变为非零值。
在主线程中,我们等待直到有文本输入,然后调用 sem_post 增加信号量的值,这将立刻令另外一个线程从 sem_wait 的等待中返回并开始执行。在统计完字符个数后,它再次调用 sem_wait 并再次被阻塞,直到主线程再次调用 sem_post 增加信号量的值为止。
我们很容易忽略程序设计上的细微错误,而该错误会导致程序运行结果中的一些细微错误。我们将上面的程序稍加修改:
#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;
res = sem_init(&bin_sem, 0, 0);
if (res != 0) {
perror("Semaphore initialization failed");
exit(EXIT_FAILURE);
}
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");
while(strncmp("end", work_area, 3) != 0) {
if (strncmp(work_area, "FAST", 4) == 0) {
sem_post(&bin_sem);
strcpy(work_area, "Wheeee...");
} else {
fgets(work_area, WORK_SIZE, stdin);
}
sem_post(&bin_sem);
}
printf("\nWaiting for thread to finish...\n");
res = pthread_join(a_thread, &thread_result);
if (res != 0) {
perror("Thread join failed");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
sem_destroy(&bin_sem);
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
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);
}
输入 FAST
$ ./thread3a
Input some text. Enter ‘end’ to finish
Excession
You input 9 characters
FAST
You input 7 characters
You input 7 characters
You input 7 characters
end
Waiting for thread to finish...
Thread joined
问题在于,我们的程序依赖其接收文本输入的时间要足够长,这样另一个线程才有时间在主线程还未准备好给它更多的单词去统计之前统计出工作区域中字符的个数。当我们试图连续快速的给它两组不同的单词去统计时(键盘输入的 FAST 和程序自动提供的 Weee...),第二个线程就没有时间去执行。但信号量已经被增加了不止一次,所以字符统计线程就会反复统计字符数目并减少信号量的值,直到它再次变为零为止。
这个例子显示:在多线程程序中,我们需要多时序考虑的非常仔细。为了解决上面程序中的问题,我们可以再增加一个信号量,让主线程等到统计线程完成字符个数的统计后再继续执行,但更简单的一种方式是使用互斥量。