Bootstrap

POSIX线程

在前面,我们介绍了如何在Linux (包括UNIX)中处理讲程。类UNIX操作系统早就具备这种多进程功能了。但有时人们认为,用fork调用来创建新进程的代价太高。在这种情况下,如果能让一个进程同时做两件事情或至少看起来是这样将会非常有用。而且,你可能希望能有两件或更多的事情以一种非常紧密的方式同时发生。这就是需要线程发挥作用的时候了。

什么是线程

在一个程序中的多个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的一个控制序列。 虽然Linux和许多其他的操作系统一样,都擅长同时运行多个进程,但迄今为止我们看到的所有程序在执行时都是作为一个单独的进程。事实上,所有的进程都至少有一个执行线程。

弄清楚fork系统调用和创建新线程之间的区别非常重要。当进程执行fork调用时,将创建出该进程的一份新副本。这个新进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行(通常)几乎完全独立于父进程。当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此也有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。

线程的概念已经出现一段时间了,但在IEEE POSIX委员会发布有关标准之前,它们并没有在类UNIX操作系统中得到广泛支持,而且已存在的线程实现版本也因厂商的不同而有所差异。POSIX1003.1c规范的发布改变了这一切,线程不仅被很好地标准化了,而且现在绝大多数Linux发行版都已支持它。现在,多核处理器即便对于台式机也已非常普遍,大多数机器在底层硬件上就已物理支持了同时执行多个线程。而此前,对于单核CPU来说,线程的同时执行只是一个聪明、但非常有效的幻觉。

Linux系统在1996年第一次获得线程的支持,我们常把当时使用的函数库称为LinuxThread。LinuxThread已经和POSIX的标准非常接近了(事实上,从许多方面来看,它们之前的区别并不明显),它是在Linux程序设计中迈出的很重要的一步,它使Linux程序员第一次可以在Linux系统中使用线程。但是,在Linux的线程实现版本和POSIX标准之间还是存在着细微的差别,最明显的是关于信号处理部分。这些差别中的大部分都受底层Linux内核的限制,而不是函数库实现所强加的。

许多项目都在研究如何才能改善Linux对线程的支持,这种改善不仅仅是清除POSIX标准和Linux具体实现之间的细微的差别,而且要增强Linux线程的性能和删除一些不需要的限制,其中大部分工作都集中在如何将用户级的线程映射到内核级的线程

本篇中的大部分代码适用于任何一种线程库,因为这些代码是基于POSIX标准的,而该标准普遍适用于所有的线程库。但是,如果使用的是旧的Linux发行版,你将看到一些细微的区别,特别是用ps命令查看示例程序的运行情况时。

线程的优点和缺点

在某些环境下,创建新线程要比创建新进程有更明显的优势。新线程的创建代价要比新进程小得多(虽然与其他一些操作系统相比,Linux在创建新进程方面的效率是很高的)。
下面是一些使用线程的优点。
有时,让程序看起来好像是在同时做两件事情是很有用的。一个经典的例子是,在编辑文档的同时对文档中的单词个数进行实时统计。一个线程负责处理用户的输入并执行文本编辑工作,另一个(它也可以看到相同的文档内容)则不断刷新单词计数变量。第一个线程(甚至可以是第三个线程)通过这个共享的计数变量让用户随时了解自己的工作进展情况。另一个例子是一个多线程的数据库服务器,这是一种明显的单进程服务多用户的情况。它会在响应一些请求的同时阻塞另外一些请求,使之等待磁盘操作,从而改善整体上的数据吞吐量。对于数据库服务器来说,这个明显的多任务工作如果用多进程的方式来完成将很难傲到高效,因为各个不同的进程必须紧密合作才能满足加锁和数据一致性方面的要求,而用多线程来完成就比用多进程要容易得多。

一个混杂着输入、计算和输出的应用程序,可以将这几个部分分离为3个线程来执行,从而改善程序执行的性能。当输入或输出线程等待连接时,另外一个线程可以继续执行。因此,如果一个进程在任一时刻最多只能做一件事情的话,线程可以让它在等待连接之类的事情的同时做一些其他有用的事情。一个需要同时处理多个网络连接的服务器应用程序也是一个天生适用于应用多线程的例子。

一般而言,线程之间的切换需要操作系统做的工作要比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程。如果一个程序在逻辑上需要有多个执行线程,那么在单处理器系统上把它运行为一个多线程程序才更符合实际情况。虽然如此,编写一个多线程程序的设计困难较大,不应等闲视之。

线程也有下面一些缺点。
编写多线程程序需要非常仔细的设计。在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发错误的可能性是很大的。Alan Cox (Linux方面的权威,他撰写了本书的序)曾经评论线程为“如何立刻让自己自讨苦吃。”

对多线程程序的调试要比对单线程程序的调试困难得多,因为线程之间的交互非常难于控制。将大量计算分成两个部分,并把这两个部分作为两个不同的线程来运行的程序在一台单处理器

机器上并不一定运行得更快,除非计算确实允许它的不同部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理。

第一个线程程序

线程有一套完整的与其有关的函数库调用,它们中的绝大多数函数名都以pthread_开头。为了使用这些函数库调用,我们必须定义宏_REENTRANT,在程序中包含头文件pthread.h,并且在编译程序时需要用选项-lpthread来链接线程库。

在设计最初的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函数。它的定义如下所示:

#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr,void*(*start_routine) (voia *) , void *arg);

这个函数定义看起来很复杂,其实用起来很简单。第一个参数是指向pthread_t类型数据的指针。

线程被创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。下一个参数用于设置线程的属性。我们一般不需要特殊的属性,所以只需设置该参数为NULL。最后两个参数分别告诉线程将要启动执行的函数和传递给该函数的参数。

void *(*start_routine)(void *)

上面一行告诉我们必须要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针。因此,可以传递一个任一类型的参数并返回一个任一类型的指针。用fork调用后,父子进程将在同一位置继续执行下去,只是fork调用的返回值是不同的;但对新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。

该函数调用成功时返回值是0,如果失败则返回错误代码。手册页对这个函数以及在本章中将要介绍的其他函数的错误条件有详细的说明。
pthread_create和大多数pthread_系列函数一样,在失败时并未遵循UNIX函数的惯例返回-1,这种情况在UNIX函数中属于一少部分。所以除非你很有把握,在对错误代码进行检查之前一定要仔细阅读使用手册中的有关内容。

线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。注意,绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不再存在了,这将引起严重的程序漏洞。pthread _exit函数的定义如下所示:

#include <pthread.h>
void pthread_exit (void *retval);

pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数。这个函数的定义如下所示:

#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return) ;

第一个参数指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。

第二个参数是一个指针,它指向另一个指针,而后者指向线程的返回值。与pthread_create类似,这个函数在成功时返回0,失败时返回错误代码。

实验一个简单的线程程序

这个程序创建一个新线程,新线程与原先的线程共享变量,并在结束时向原先的线程返回一个结果。没有比这更简单的多线程程序了!下面是程序thread1.c的代码:

/* thread1.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void *thread_function(void *arg);
char message[] ="Hello wor1d";
             
int main()
{
   
	int res;
	pthread_t a_thread;
    void *thread_result;
	res = pthread_create(&a_thread,NULL,thread_function,(void *) message);
    if(res != 0)
    {
   
		perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
	printf("waiting 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, it returned %s\n",(char *)thread_result);
    printf("Message is now %s\n",message);
	exit(EXIT_SUCCESS);
}
void *thread_function(void *arg)
{
   
	printf("thread_function is running.Argument was %s\n",(char*)arg);
    sleep(3);
	strcpy(message, "Bye!");
	pthread_exit("Thank you for the CPU time");
}

在这里插入图片描述

首先,我们定义了在创建线程时需要由它调用的一个函数的原型。如下所示:void *thread_function (void *arg);
根据pthread_create的要求,它只有一个指向void的指针作为参数,返回的也是指向void的指针。稍后,我们将介绍这个函数的实现。
在main函数中,我们首先定义了几个变量,然后调用pthread_create开始运行新线程。如下所示:

pthread_t a_thread;
void * thread_result;
res = pthread_create(&a_thread,NULL,thread_function,(void *)message);

我们向pthread_create函数传递了一个pthread_t类型对象的地址,今后可以用它来引用这个新线程。我们不想改变默认的线程属性,所以设置第二个参数为NULL。最后两个参数分别为将要调用的函数和一个传递给该函数的参数。
如果这个调用成功了,就会有两个线程在运行。原先的线程(main)继续执行pthread_create后面的代码,而新线程开始执行thread_function函数。
原先的线程在查明新线程已经启动后,将调用pthread_join函数,如下所示:res = pthread_join(a_thread,&thread_result) ;
我们给该函数传递两个参数,一个是正在等待其结束的线程的标识符,另一个是指向线程返回值的指针。这个函数将等到它所指定的线程终止后才返回。然后主线程将打印新线程的返回值和全局变量message的值,最后退出。

新线程在thread_function函数中开始执行,它先打印出自己的参数,休眠一会儿,然后更新全局变量,最后退出并向主线程返回一个字符串。新线程修改了数组message,而原先的线程也可以访问该数组。如果我们调用的是fork而不是pthread_create,就不会有这样的效果。

同时执行

接下来,我们将编写一个程序来验证两个线程的执行是同时进行的(当然,如果是在一个单处理器系统上,线程的同时执行就需要靠CPU在线程之间的快速切换来实现)。因为还未介绍到任何可以帮助我们有效地完成这一工作的线程同步函数,在这个程序中我们是在两个线程之间使用轮询技术,所以它的效率很低。同时,我们的程序仍然要利用这一事实,即除局部变量外,所有其他变量都将在一个进程中的所有线程之间共享。

实验两个线程同时执行
在本节中,我们创建的程序thread2 .c是在对thread1 .c稍加修改的基础上编写出来的。我们增加了另外一个全局变量来测试哪个线程正在运行。如下所示:

/* thread2.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void *thread_function(void *arg);
char message[] ="Hello wor1d";
int run_now = 1;          
int main()
{
   
	int res;
	pthread_t a_thread;
	res = pthread_create(&a_thread,NULL,thread_function,(void *) message);
    if( res != 0)
    {
   
        printf("thread created was failed \n");
        exit(EXIT_FAILURE);
	}
	int print_count1 = 0;
	while (print_count1++ < 20) 
    {
   
		if (run_now == 1)
        {
   
			printf("1");
			run_now = 2;
        }
		else
        {
   
			sleep(1);
        }
    
;