POSIX线程 (1)

来源:互联网 发布:淘宝女童模特可可真名 编辑:程序博客网 时间:2024/04/28 23:01

2011-11-06 wcdj


BLP 4th P.416

类UNIX操作系统早就具备了多进程的功能了,但有时人们认为,用fork调用来创建新进程的代价太高。在这种情况下,如果能让一个进程同时做两件事情或至少看起来是这样将会非常有用。而且,你可能希望能有两件或更多的事情以一种非常紧密的方式同时发生。这就是需要线程发挥作用的时候了。
知识点:
(1) 在进程中创建新线程。
pthread_create 函数
pthread_exit 函数
pthread_join 函数
(2) 在一个进程中同步线程之间的数据访问。
(3) 修改线程的属性。
(4) 在同一个进程中,从一个线程中控制另一个线程。

1 什么是线程

在一个程序中的多个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的一个控制序列。虽然Linux和许多其他的操作系统一样,都擅长同时运行多个进程,但迄今为止我们看到的所有程序在执行时都是作为一个单独进程。事实上,所有的进程都至少有一个执行线程。
注意:
弄清楚fork系统调用和创建新线程之间的区别非常重要。
(1) 当进程执行fork调用时,将创建出该进程的一份新副本。这个新进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行(通常)几乎完全独立于父进程。
(2) 当在进程中创建一个新线程时,新的执行线程将拥有自己的栈(因此,也有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。

现在,多核处理器已非常普遍,大多数机器在底层硬件上就已物理支持了同时执行多个线程。而此前,对于单核CPU来说,线程的同时执行只是一个聪明、但非常有效的幻觉。
Linux在1996年第一次获得线程的支持,我们常把当时使用的函数库称为LinuxThread。LinuxThread已经和POSIX的标准非常接近了(事实上,从许多方面来看,它们之前的区别并不明显),它使Linux程序员第一次可以在Linux系统中使用线程。但是,在Linux的线程实现版本中和POSIX标准之间还是存在着细微的差别,最明显的是关于信号处理部分。这些差别中的大部分都受底层Linux内核的限制,而不是函数库实现所强加的。

NPTL(Native POSIX Thread Library, Linux上的本地POSIX线程库)将成为Linux线程的新标准。第一个NPTL的主流版本出现在Red Hat Linux版本9上。关于NPTL的背景资料可以参考“Linux上的本地POSIX线程库”,作者是Ulrich Drepper和Ingo Molnar。
下载地址:http://people.redhat.com/drepper/nptl-design.pdf

2 线程的优点和缺点

在某些情况下,创建新线程要比创建新进程更有明显的优势。新线程的创建代价要比新进程小得多(虽然与其他一些操作系统相比,Linux在创建新进程方面的效率是很高的)。
使用线程的 —— 优点
(1) 有时,让程序看起来好像是在同时做两件事情是很有用的。
多任务的工作如果用多进程的方式来完成将很难做到高效,因为各个不同进程必须紧密合作才能满足加锁和数据一致性方面的要求,而用多线程来完成就比用多进程要容易得多。
(2) 一个混杂着输入、计算和输出的应用程序,可以将这几个部分分离为3个线程来执行,从而改善程序执行的性能。一个需要同时处理多个网络连接的服务器应用程序也是一个天生适用于应用多线程的例子。
(3) 一般而言,线程之间的切换需要操作系统做的工作要比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程。
使用线程的 —— 缺点
(1) 编写多线程程序需要非常仔细的设计。在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发的错误的可能性是很大的。
(2) 对多线程程序的调试要比对单线程程序的调试困难得多,因为线程之间的交互非常难于控制。
(3) 将大量计算分成两个部分,并把这两个部分作为两个不同的线程来运行的程序,在一台单处理器机器上并不一定运行得更快,除非计算确实允许它的不同部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理。

3 第一个线程程序

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

在设计最初的UNIX和POSIX库例程时,人们假设每个进程中只有一个执行线程。一个明显的例子就是errno,该变量用于获取某个函数调用失败后的错误信息。在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个全局性区域来缓存输出数据。
为了解决这个问题,我们需要使用被称为 —— 可重入的例程。可重入代码可以被多次调用而仍能正常工作。这些调用可以来自不同的线程,也可以是某种形式的嵌套调用。因此,代码中的可重入部分通常只使用局部变量,这使得每次对该代码的调用都将获得它自己的唯一的一份数据副本。

编写多线程程序时,我们通过定义 _REENTRANT 来告诉编译器我们需要可重入功能,这个宏的定义必须位于程序中的任何 #include 语句之前。它将为我们做3件事,并且做得非常优雅,以至于我们一般不需要知道它到底做了哪些事:
(1) 它会对部分函数重新定义它们的可安全重入的版本,这些函数的名字一般不会发生改变,只是会在函数名后面添加 _r 字符串。例如,函数名 gethostbyname 将变为 gethostbyname_r。
(2) stdio.h 中原来以宏的形式实现的一些函数将变成可安全重入的函数。
(3) 在errno.h中定义的变量errno现在将成为一个函数调用,它能够以一种多线程安全的方式来获取真正的errno值。

pthread_create 函数
首先来看一个用于管理线程的新函数 pthread_create,它的作用是创建一个新线程,类似于创建新进程的 fork 函数。它的定义为:
#include <pthread.h>
int pthread_create( pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg );

参数解释:
第一个参数是指向pthread_t类型数据的指针。线程被创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。
第二个参数是用于设置线程的属性。一般不需要特殊的属性,所以只需设置该参数为NULL。
第三个参数是告诉线程将要启动执行的函数。
第四个参数是传递给执行函数的参数。

注意:
(1) void *(* start_routine)(void *)
第三个参数要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针。因此,可以传递一个任一类型的参数并返回一个任一类型的指针。
(2) 区别:用 fork 调用厚,父子进程将在同一位置继续执行下去,只是 fork 调用的返回值是不同的;但对新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。
(3) pthread_create函数的调用成功时,返回值是0,如果失败则返回错误代码。详细可以参考手册页。
PS: pthread_create和大多数pthread_系列函数一样,在失败时并未遵循UNIX函数的习惯返回-1,这种情况在UNIX函数中属于一少部分。所以,除非你很有把握,在对错误码代码进行检查之前一定要仔细阅读使用手册中的有关内容。

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


pthread_join 函数
pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数。
pthread_join函数的定义为:
#include <pthread.h>
int pthread_join( pthread_t th, void **thread_return );

第一个参数是:指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。
第二个参数是:一个指针,它指向另一个指针,而后者指向线程的返回值。
返回值:与pthread_create类似,这个函数在成功时返回0,失败时返回错误代码。

例子:一个简单的线程程序
这个程序创建一个新线程,新线程与原先的线程共享变量,并在结束时向原先的线程返回一个结果。

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <pthread.h>// pthread headervoid *thread_func(void *arg);// thread functionchar msg[] = "hi wcdj";// shared between main thread and new threadint main() {int res;pthread_t a_thread;void *thread_result;res = pthread_create(&a_thread, NULL, thread_func, (void *)msg);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", msg);exit(EXIT_SUCCESS);}// Thread functionvoid *thread_func(void *arg){printf("thread_func is running. Argument was %s\n", (char *)arg);sleep(3);strcpy(msg, "Bye, wcdj");// modify the shared variablepthread_exit("Thank u 4 the CPU time");}
编译程序:
pic


待续……