2011-11-27 wcdj
BLP 4th P.421
1 同时执行
2 同步
2.1 用信号量进行同步
2.2 用互斥量进行同步
1 同时执行
编写一个程序来验证两个线程程序是同时进行的(当然,如果是一个单处理器系统上,线程的同时执行就需要靠CPU在线程之间的快速切换来实现)。在这个程序中我们是在两个线程之间使用“轮询技术”,所以它的效率很低。
问题1:两个线程同时执行
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM 20
void *thread_function(void *arg);
int run_now = 1;
char message[] = "hi, wcdj";
int main()
{
int res;
pthread_t a_thread;
void *thread_result;
int print_count1 = 0;
res = pthread_create(&a_thread, NULL, thread_function, (void *)message);
if (res != 0)
{
perror("Thread creation failed");
}
while(print_count1++ < NUM)
{
if(run_now == 1)
{
printf("1");
run_now = 2;
}
else
{
sleep(1);
}
}
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");
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
int print_count2 = 0;
while(print_count2++ < NUM)
{
if (run_now == 2)
{
printf("2");
run_now = 1;
}
else
{
sleep(1);
}
}
sleep(3);
}
/*
编译执行:
gcc -D_REENTRANT thread2.c -o thread2 -lpthread
$./thread2
输出结果:
12121212121212121212
Waiting for thread to finish...
Thread joined
*/
解释:
笨方法:如果run_now的值为1,就打印“1”并设置它为2,否则,就稍作休息然后再检查它的值。我们不断地检查来等待它的值变为1,这种方式被称为“忙等待”。虽然已经在两次检查之间休息1s来减慢检查的频率了。
每个线程通过设置run_now变量的方法来通知另一个线程开始运行,然后,它会等待另一个线程改变了这个变量的值后再次运行。这个例子显示了两个线程之间自动交替执行,同时也再次阐明了一个观点:即,这两个线程共享run_now变量(也就是,除局部变量外,所有其他变量都将在一个进程中的所有线程之间共享)。
2 同步
在上一部分,我们看到两个线程同时执行的情况,但我们采用的在它们之间进行切换的方法是非常笨拙且没有效率的。幸运的是,专门有一组设计好的函数为我们提供了更好的控制线程执行和访问代码临界区域的方法。
两种基本方法:
(1) 信号量
它的作用如同看守一段代码的看门人;
(2) 互斥量
它的作用如同保护代码段的一个互斥设备;
这两种方法很相似,事实上,它们可以互相通过对方来实现。但在实际应用中,对于一些情况,可能使用信号量或互斥量中的一个更符合问题的语义,并且效果更好。
例如:如果想控制任一时刻只能有一个线程可以访问一些共享内存,使用互斥量就要自然得多。但在控制对一组相同对象的访问时 —— 比如从5条可用的电话线中分配1条给某个线程的情况,就更适合使用计数信号量。具体选择哪种方法取决于个人偏好和相应的程序机制。
2.1 用信号量进行同步
有两组接口函数用于信号量。
(1) 一组取自POSIX的实时扩展,用于线程。
(2) 另一组被称为系统V信号量,常用于进程的同步(以后介绍这个接口)。
这两组接口函数虽然很相似,但并不保证它们之间可以互换,而且它们使用的函数调用也各不相同。
荷兰计算机科学家Dijkstra(迪杰斯特拉)首先提出了信号量的概念。
信号量:是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此。这意味着如果一个程序中有两个或更多的线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。但如果是普通变量,来自同一程序中的不同线程的冲突操作所导致的结果将是不确定的。
二进制信号量和计数信号量
这里介绍一种最简单的信号量 —— 二进制信号量,它只有0和1两种取值。还有一种更通用的信号量 —— 计数信号量,它可以有更大的取值范围。
信号量一般常用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作,就要使用二进制信号量。有时,我们希望可以允许有限数目的线程执行一段指定的代码,这就需要用到计数信号量。由于计数信号量并不常用,所以在这里不对它进行深入的介绍,实际上它仅仅是二进制信号量的一种逻辑扩展,两者实际调用的函数都一样。
信号量函数的名字都以 sem_ 开头。线程中使用的基本信号量函数有4个:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_destory(sem_t *sem);
(1) sem_init函数初始化由sem指向的信号量对象,设置它的共享选项,并给它一个初始的整数值。pshared参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享。
(2)
sem_post函数的作用是以原子操作的方式给信号量的值加1。
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。
原子操作是指,如果两个线程企图同时给一个信号量加1,它们之间不会相互干扰,而不像如果两个程序同时对同一个文件进行读取、增加、写入操作时可能会引起冲突。信号量的值总是会被正确地加2,因为有两个线程试图改变它。
PS: 还有另外一个信号量函数 sem_trywait,它是sem_wait的非阻塞版本。详细资料参考它的手册页。
(3) sem_destory函数的作用是,用完信号量后对它进行清理。如果企图清理的信号量正被一些线程等待,就会收到一个错误。
问题2:一个线程信号量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.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);// add 1
}
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);// clear
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
sem_wait(&bin_sem);// subtract 1
while(strncmp("end", work_area, 3) != 0)
{
printf("You input [%d] characters\n", strlen(work_area)-1);
sem_wait(&bin_sem);// subtract 1
}
pthread_exit(NULL);
}
注意:
(1) 在创建新线程之前对信号量进行初始化,并将这个信号量的初始值置为0。这样,在线程函数启动时,sem_wait函数调用就会阻塞并等待信号量变为非零值。
(2) 在主线程中,等待直到有文本输入,然后调用sem_post增加信号量的值,这将立刻令另一个线程从sem_wait的等待中返回并开始执行。在统计完字符个数之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值为止。
问题3:将问题2中来自键盘的输入用事先准备好的文本自动替换掉,上面的程序的结果会有什么变化?
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.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)
{
// fast input
if (strncmp(work_area, "FAST", 4) == 0)
{
strcpy(work_area, "My name is wcdj\n");
sem_post(&bin_sem);// add 1
}
else
{
fgets(work_area, WORK_SIZE, stdin);
}
sem_post(&bin_sem);// add 1
}
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);// clear
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
sem_wait(&bin_sem);// subtract 1
while(strncmp("end", work_area, 3) != 0)
{
printf("You input [%d] characters\n", strlen(work_area)-1);
sem_wait(&bin_sem);// subtract 1
}
pthread_exit(NULL);
}
解释:
现在,如果输入FAST,程序就会调用sem_post使字符统计线程开始运行,同时立刻用其他数据更新work_area数组。
问题来了:我们的程序依赖其接收文本输入的时间要足够长,这样另一个线程才有时间在主线程还未准备好给它更多的单词去统计之前统计出work_area中字符的个数。当我们试图连续快速地给它两组不同的单词去统计时,第二个线程就没有时间去执行(即,在本例中,只对最后一次改变的"My name is wcdj"进行字符统计),但信号量已被增加了不止一次(即,这里总共增加了3次,然后再次阻塞到fgets函数),所以字符统计线程就会反复统计字符数目并减少信号量的值,直到它再次变为0为止。
结论:
这个例子显示了:在多线程程序中,我们需要对 “时序”考虑得非常仔细。为了解决上面程序中的问题,我们可以再增加一个信号量,让主线程等到统计线程完成字符个数的统计后再继续执行,但更简单的一种方式是使用互斥量。
方法1:再增加一个信号量,控制主线程等待子线程完成统计后再继续执行
下面是一种实现方法:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
void *thread_function(void *arg);
sem_t bin_sem;
sem_t another_sem;// control main thread to wait for child thread
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int main()
{
int res;
int res2;
pthread_t a_thread;
void *thread_result;
res = sem_init(&bin_sem, 0, 0);
res2 = sem_init(&another_sem, 0, 1);// initial value is 1
if (res != 0 || res2 != 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)
{
sem_wait(&another_sem);// wait child thread to finish
// fast input
if (strncmp(work_area, "FAST", 4) == 0)
{
strcpy(work_area, "My name is wcdj\n");
sem_post(&bin_sem);// add 1
continue;
}
else
{
fgets(work_area, WORK_SIZE, stdin);
}
sem_post(&bin_sem);// add 1
}
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); // clear
sem_destroy(&another_sem); // clear
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
sem_wait(&bin_sem);// subtract 1
while(strncmp("end", work_area, 3) != 0)
{
printf("You input [%d] characters\n", strlen(work_area)-1);
sem_post(&another_sem);//add 1
sem_wait(&bin_sem);// subtract 1
}
pthread_exit(NULL);
}
方法2:使用互斥量
参考下节。
2.2 用互斥量进行同步
另一种用在多线程程序中同步访问方法是使用互斥量。它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。
用于互斥量的基本函数和用于信号量的函数非常相似:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
(1) 与其他函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,你必须对函数的返回代码进行检查。
(2) pthread_mutex_init函数的属性参数允许我们设置互斥量的属性,而属性控制着互斥量的行为。属性类型默认为fast,但它有一个小缺点:如果程序试图对一个已经加了锁的互斥量调用pthread_mutex_lock,程序就会被阻塞,而又因为拥有互斥量的这个线程正是现在被阻塞的线程,所以互斥量就永远也不会被解锁了,程序也就进入死锁状态。—— 这个问题可以通过改变互斥量的属性来解决,我们可以让它检查这种情况并返回一个错误,或者让它递归的操作,给同一个线程加上多个锁,但必须注意在后面执行同等数量的解锁操作。
问题4:线程互斥量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_function(void *arg);
// protects both work_area and time_to_exit
pthread_mutex_t work_mutex;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;
int main()
{
int res;
pthread_t a_thread;
void *thread_result;
res = pthread_mutex_init(&work_mutex, NULL);// init
if (res != 0)
{
perror("Mutex initialization failed");
exit(EXIT_FAILURE);
}
res = pthread_create(&a_thread, NULL, thread_function, NULL);
if (res != 0)
{
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
pthread_mutex_lock(&work_mutex);// lock
printf("Input some text. Enter 'end' to finish\n");
while (!time_to_exit)// check
{
fgets(work_area, WORK_SIZE, stdin);
pthread_mutex_unlock(&work_mutex);// unlock
while(1)
{
pthread_mutex_lock(&work_mutex);// lock
if (work_area[0] != '\0')
{
pthread_mutex_unlock(&work_mutex);
sleep(1);
}
else
{
break;
}
}
}
pthread_mutex_unlock(&work_mutex);// unlock
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");
pthread_mutex_destroy(&work_mutex);
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
sleep(1);
pthread_mutex_lock(&work_mutex);// lock
while(strncmp("end", work_area, 3) != 0)
{
printf("You input %d characters\n", strlen(work_area)-1);
work_area[0] = '\0';// mark after finish
pthread_mutex_unlock(&work_mutex);// unlock
sleep(1);
pthread_mutex_lock(&work_mutex);// lock
while (work_area[0] == '\0')
{
pthread_mutex_unlock(&work_mutex);// unlock
sleep(1);
pthread_mutex_lock(&work_mutex);// lock
}
}
time_to_exit = 1;// mark end
work_area[0] = '\0';
pthread_mutex_unlock(&work_mutex);
pthread_exit(0);
}
注意:
这种通过“轮询”来获得结果的方法通常并不是好的编程方式。在实际的编程中,我们应该尽可能用信号量来避免出现这种情况。这里的代码只是用作示例而已。