现在让我们看一个和多进程版本相似的闹钟程序,但它是用多线程实现的。该例子中用到的三个Pthreads函数:
●pthread_create : 创建一个线程,运行由第三个参数(alarm_thread)指定的例程(具体见下面例子),并返回线程标识符ID(保存在thread引用的变量中)
● pthread_detach : 当线程终止时立刻回收线程资源
● pthread_exit: 终止线程调用
数据结构alarm_t中定义了每个闹钟的命令信息,seconds中存储等待时间,message中存储显示文本。
#include <pthread.h>
#include "errors.h"
typedef struct alarm_tag {
int seconds;
char message[64];
} alarm_t;
函数alarm_thread是闹钟线程,即创建的每个闹钟线程执行的函数,该函数返回时,闹钟线程终止。该函数参数(void *arg)是传给pthread_create函数的第四个参数,即alarm_t结构体指针。线程首先将void* 参数转为alarm_t* 类型,然后调用pthread_detach函数来分离自己,作用是通知Pthreads不必关心它的终止时间与退出状态。
线程睡眠指定的时间(由alarm_t 中的seconds决定),之后打印指定的消息文本,最后释放alarm_t结构体空间并返回,线程终止。通常,Pthreads会保存线程的资源以供其他线程了解它已经终止并获得其最终结果。由于本例中线程负责分离自己,所以不必做上述工作。
void *alarm_thread (void *arg)
{
alarm_t *alarm = (alarm_t*)arg;
int status;
status = pthread_detach (pthread_self ());
if (status != 0)
err_abort (status, "Detach thread");
sleep (alarm->seconds);
printf ("(%d) %s\n", alarm->seconds, alarm->message);
free (alarm);
return NULL;
}
线程版本闹钟的main()函数与之前的两个版本相同,循环读取命令行、解析命令行直到不能从stdin中读取数据为止。
创建一个闹钟线程,它以alarm_t为线程参数运行函数alarm_thread。
int main()
{
int status;
char line[128];
alarm_t *alarm;
pthread_t thread;
while (1) {
printf ("Alarm> ");
if (fgets (line, sizeof (line), stdin) == NULL) exit (0);
if (strlen (line) <= 1) continue;
alarm = (alarm_t*)malloc (sizeof (alarm_t));
if (alarm == NULL)
errno_abort ("Allocate alarm");
if (sscanf (line, "%d %64[^\n]",
&alarm->seconds, alarm->message) < 2) {
fprintf (stderr, "Bad command\n");
free (alarm);
} else {
status = pthread_create (
&thread, NULL, alarm_thread, alarm);
if (status != 0)
err_abort (status, "Create alarm thread");
}
}
}
总结:比较两个异步版本闹钟程序是理解线程编程很好的选择。在fork版本中,每个闹钟有一个从主进程拷贝的独立地址空间,这意味着可以将闹钟时间和显示文本放在局部变量中,一旦创建了子进程,父进程就可以改变这些变量而不会影响闹钟子进程。在多线程版本中,所有线程共享同一个地址空间,所以可为每个闹钟调用malloc建立新的alarm_t结构体,并传递给新建线程。
在使用fork()版本中,主进程要调用waitpid函数来通知系统释放其创建的子进程资源。在多线程版本中,不需要等待线程结束,除非希望获得它的返回值;每个线程分离自己,故该线程的资源在它终止后会立刻回收。
在实际应用中,不会为每个闹钟建立一个进程。你可能轻易设置上百个闹钟活动,但是系统可能无法创建那么多进程。但是对应可以在一个进程中创建几百个线程。
另外可以将常用的头文件以及一些宏定义包含在一个头文件中,比如#include "errors.h"。本次程序的运行环境依然是Qt的控制台程序。
彩蛋:一个更加成熟的闹钟版本可以只有两个线程:一个负责读取用户输入,一个等待闹钟停止。之后的学习会逐步实现该版本。