12.3 第一个线程程序
线程有一套完整的与其有关的函数库调用,它们中的绝大多数函数名都以pthread_开头。为了使用这些函数库调用,我们必须定义宏_REENTRANT,在程序中包含头文件pthread.h,并且在编译程序时需要用选项-1pthread来链接线程库。
在设计最初的UNIX和POSIX库例程时,人们假设每个进程中只有一个执行线程。一个明显的例子就是errno,该变量用于获取某个函数调用失败后的错误信息。在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个全局性区域来缓存输出数据。
为解决这个问题,我们需要使用被称为可重入的例程。可重入代码可以被多次调用而仍然正常工作,这些调用可以来自不同的线程,也可以是某种形式的嵌套调用。因此,代码中的可重入部分通常只使用局部变量,这使得每次对该代码的调用都将获得它自己的唯一的一份数据副本。
编写多线程程序时,我们通过定义宏_REENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须位于程序中的任何#include语句之前。它将为我们做3件事情,并且做得非常优雅,以至于我们一般不需要知道它到底做了哪些事。
❑ 它会对部分函数重新定义它们的可安全重入的版本,这些函数的名字一般不会发生改变,只是会在函数名后面添加_r字符串。例如,函数名gethostbyname将变为gethostbyname_r。
❑ stdio.h中原来以宏的形式实现的一些函数将变成可安全重入的函数。
❑ 在errno.h中定义的变量errno现在将成为一个函数调用,它能够以一种多线程安全的方式来获取真正的errno值。
在程序中包含头文件pthread.h还将向我们提供一些其他的将在代码中使用到的定义和函数原型,就如同头文件stdio.h为标准输入和标准输出例程所提供的定义一样。最后,需要确保在程序中包含了正确的线程头文件,并且在编译程序时链接了实现pthread函数的正确的线程库。有关编译线程程序的更详细的情况将在下面的实验部分中再介绍。现在,我们首先来看一个用于管理线程的新函数pthread_create,它的作用是创建一个新线程,类似于创建新进程的fork函数。它的定义如下所示:
这个函数定义看起来很复杂,其实用起来很简单。第一个参数是指向pthread_t类型数据的指针。线程被创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。下一个参数用于设置线程的属性。我们一般不需要特殊的属性,所以只需设置该参数为NULL。我们将在本章的后面介绍如何使用这些属性。最后两个参数分别告诉线程将要启动执行的函数和传递给该函数的参数。
上面一行告诉我们必须要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针。因此,可以传递一个任一类型的参数并返回一个任一类型的指针。用fork调用后,父子进程将在同一位置继续执行下去,只是fork调用的返回值是不同的;但对新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。
该函数调用成功时返回值是0,如果失败则返回错误代码。手册页对这个函数以及在本章中将要介绍的其他函数的错误条件有详细的说明。
pthreadcreate和大多数pthread系列函数一样,在失败时并未遵循UNIX函数的惯例返回-1,这种情况在UNIX函数中属于一少部分。所以除非你很有把握,在对错误代码进行检查之前一定要仔细阅读使用手册中的有关内容。
线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。注意,绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不再存在了,这将引起严重的程序漏洞。pthread_exit函数的定义如下所示:
pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数。这个函数的定义如下所示:
第一个参数指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。第二个参数是一个指针,它指向另一个指针,而后者指向线程的返回值。与pthread_create类似,这个函数在成功时返回0,失败时返回错误代码。
实 验 一个简单的线程程序
这个程序创建一个新线程,新线程与原先的线程共享变量,并在结束时向原先的线程返回一个结果。没有比这更简单的多线程程序了!下面是程序thread1.c的代码:
(1)编译这个程序时,我们首先需要定义宏_REENTRANT。在少数系统上,可能还需要定义宏_POSIX_C_SOURCE,但一般不需要定义它。
(2)接下来必须链接正确的线程库。如果使用的是一个老的Linux发行版,默认的线程库不是NPTL,你可能需要升级Linux发行版,尽管本章中的大多数代码也兼容老的Linux线程实现。简单的检查方法是查看头文件/usr/include/pthread.h。如果这个文件中显示的版权日期是2003年或更晚,那几乎可以肯定你的Linux发行版使用的是NPTL实现。如果日期比这个早,你可能就需要安装一个较新版本的Linux了。
(3)在验证并安装了正确的文件后,现在可以编译和链接这个程序了,使用的命令如下所示:
如果你的系统默认使用的(很有可能)就是NPTL线程库,那么编译程序时就无需加上-I和-L选项。使用的命令如下所示:
我们将在本章中一直使用这一简单版本的命令行。
(4)运行这个程序时,你将看到:
这个程序值得我们花一点时间去理解,因为它是本章中大多数例子的基础。
实验解析
首先,我们定义了在创建线程时需要由它调用的一个函数的原型。如下所示:
根据pthread_create的要求,它只有一个指向void的指针作为参数,返回的也是指向void的指针。稍后,我们将介绍这个函数的实现。
在main函数中,我们首先定义了几个变量,然后调用pthread_create开始运行新线程。如下所示:
我们向pthread_create函数传递了一个pthread_t类型对象的地址,今后可以用它来引用这个新线程。我们不想改变默认的线程属性,所以设置第二个参数为NULL。最后两个参数分别为将要调用的函数和一个传递给该函数的参数。
如果这个调用成功了,就会有两个线程在运行。原先的线程(main)继续执行pthread_create后面的代码,而新线程开始执行thread_function函数。
原先的线程在查明新线程已经启动后,将调用pthread_join函数,如下所示:
我们给该函数传递两个参数,一个是正在等待其结束的线程的标识符,另一个是指向线程返回值的指针。这个函数将等到它所指定的线程终止后才返回。然后主线程将打印新线程的返回值和全局变量message的值,最后退出。
新线程在thread_function函数中开始执行,它先打印出自己的参数,休眠一会儿,然后更新全局变量,最后退出并向主线程返回一个字符串。新线程修改了数组message,而原先的线程也可以访问该数组。如果我们调用的是fork而不是pthread_create,就不会有这样的效果。