- 浏览: 49908 次
- 性别:
- 来自: 北京
文章分类
POSIX 线程详解(1)
——一种支持内存共享的简捷工具
2000 年 7 月 01 日
POSIX(可移植操作系统接口)线程是提高代码响应和性能的有力手段。在本系列中,Daniel Robbins 向您精确地展示在编程中如何使用线程。其中还涉及大量幕后细节,读完本系列文章,您完全可以运用 POSIX 线程创建多线程程序。
了解如何正确运用线程是每一个优秀程序员必备的素质。线程类似于进程。如同进程,线程由内核按时间分片进行管理。在单处理器系统中,内核使用时间分片来模拟线程的并发执行,这种方式和进程的相同。而在多处理器系统中,如同多个进程,线程实际上一样可以并发执行。
那么为什么对于大多数合作性任务,多线程比多个独立的进程更优越呢?这是因为,线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果曾用 fork() 编写过重要代码,就会认识到这个工具的重要性。 为什么呢?虽然 fork() 允许创建多个进程,但它还会带来以下通信问题: 如何让多个进程相互通信,这里每个进程都有各自独立的内存(地址 )空间。对这个问题没有一个简单的答案。虽然有许多不同种类的本地IPC (进程间通信),但它们都遇到两个重要障碍:
- 强加了某种形式的额外内核开销,从而降低性能。
- 对于大多数情形,IPC 不是对于代码的“自然”扩展。通常极大地增加了程序的复杂性。
双重坏事: 开销和复杂性都非好事。如果曾经为了支持 IPC 而对程序大动干戈过,那么您就会真正欣赏线程提供的简单共享内存机制。由于所有的线程都驻留在同一内存空间,POSIX 线程无需进行开销大而复杂的长距离调用。只要利用简单的同步机制,程序中所有的线程都可以读取和修改已有的数据结构。 而无需将数据经由文件描述符转储或挤入紧窄的共享内存空间。仅此一个原因,就足以让您考虑应该采用单进程/多线程模式而非多进程/单线程模式。
不仅如此。线程同样还是非常快捷的。与标准 fork() 相比,线程带来的开销很小。内核无需单独复制进程的内存空间或文件描述符等等。这就节省了大量的 CPU 时间,使得线程创建比新进程创建快上十到一百倍。因为这一点,可以大量使用线程而无需太过于担心带来的 CPU 或内存不足。使用 fork() 时导致的大量 CPU 占用也不复存在。这表示只要在程序中有意义,通常就可以创建线程。
当然,和进程一样,线程将利用多 CPU。如果软件是针对多处理器系统设计的,这就真的是一大特性(如果软件是开放源码,则最终可能在不少平台上运行)。特定类型线程程序(尤其是 CPU 密集型程序)的性能将随系统中处理器的数目几乎线性地提高。如果正在编写 CPU 非常密集型的程序,则绝对想设法在代码中使用多线程。一旦掌握了线程编码,无需使用繁琐的 IPC 和其它复杂的通信机制,就能够以全新和创造性的方法解决编码难题。所有这些特性配合在一起使得多线程编程更有趣、快速和灵活。
如果熟悉 Linux 编程,就有可能知道 __clone() 系统调用。__clone() 类似于 fork(),同时也有许多线程的特性。例如,使用 __clone(),新的子进程可以有选择地共享父进程的执行环境(内存空间,文件描述符等)。这是好的一面。但 __clone() 也有不足之处。正如__clone() 在线帮助指出:
“__clone 调用是特定于 Linux 平台的,不适用于实现可移植的程序。欲编写线程化应用程序(多线程控制同一内存空间),最好使用实现 POSIX 1003.1c 线程 API 的库,例如 Linux-Threads 库。参阅 pthread_create(3thr)。”
虽然 __clone() 有线程的许多特性,但它是不可移植的。 当然这并不意味着代码中不能使用它。但在软件中考虑使用 __clone() 时应当权衡这一事实。值得庆幸的是,正如 __clone() 在线帮助指出,有一种更好的替代方案:POSIX 线程。如果想编写 可移植的 多线程代码,代码可运行于 Solaris、FreeBSD、Linux 和其它平台,POSIX 线程是一种当然之选。
如下示例程序中用到的线程函数:
线程创建函数: 所在头文件: #include <pthread.h> 函数原型: int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) 参数含义: thread:线程标识符 attr:线程属性设置 start_routine:线程函数起始地址 arg:传递给start_routine的参数 等待线程退出函数: 所在头文件: #include <pthread.h> 函数原型: int pthread_join( pthread_t *th,void **thread_return ) 参数含义: th:等待线程的标识符 thread_return:用户定义指针,用来存储被等待线程的返回值
下面是一个 POSIX 线程的简单示例程序:
thread1.c
#include <pthread.h> #include <stdlib.h> #include <unistd.h> void *thread_function(void *arg) { int i; for ( i=0; i<20; i++) { printf("Thread says hi!\n"); sleep(1); } return NULL; } int main(void) { pthread_t mythread; if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { printf("error creating thread."); abort(); } if ( pthread_join ( mythread, NULL ) ) { printf("error joining thread."); abort(); } exit(0); }
要编译这个程序,只需先将程序存为 thread1.c,然后输入:
$ gcc thread1.c -o thread1 -lpthread
运行则输入:
$ ./thread1
thread1.c 是一个非常简单的线程程序。虽然它没有实现什么有用的功能,但可以帮助理解线程的运行机制。下面,我们一步一步地了解这个程序是干什么的。main() 中声明了变量 mythread,类型是 pthread_t。pthread_t 类型在 pthread.h 中定义,通常称为“线程 id”(缩写为 "tid")。可以认为它是一种线程句柄。
mythread 声明后(记住 mythread 只是一个 "tid",或是将要创建的线程的句柄),调用 pthread_create 函数创建一个真实活动的线程。不要因为 pthread_create() 在 "if" 语句内而受其迷惑。由于 pthread_create() 执行成功时返回零而失败时则返回非零值,将 pthread_create() 函数调用放在 if() 语句中只是为了方便地检测失败的调用。让我们查看一下 pthread_create 参数。第一个参数 &mythread 是指向 mythread 的指针。第二个参数当前为 NULL,可用来定义线程的某些属性。由于缺省的线程属性是适用的,只需将该参数设为 NULL。
第三个参数是新线程启动时调用的函数名。本例中,函数名为 thread_function()。当 thread_function() 返回时,新线程将终止。本例中,线程函数没有实现大的功能。它仅将 "Thread says hi!" 输出 20 次然后退出。注意 thread_function() 接受 void * 作为参数,同时返回值的类型也是 void *。这表明可以用 void * 向新线程传递任意类型的数据,新线程完成时也可返回任意类型的数据。那如何向线程传递一个任意参数?很简单。只要利用 pthread_create() 中的第四个参数。本例中,因为没有必要将任何数据传给微不足道的 thread_function(),所以将第四个参数设为 NULL。
您也许已推测到,在 pthread_create() 成功返回之后,程序将包含两个线程。等一等, 两个 线程? 我们不是只创建了一个线程吗?不错,我们只创建了一个线程。但是主程序同样也是一个线程。可以这样理解:如果编写的程序根本没有使用 POSIX 线程,则该程序是单线程的(这个单线程称为“主”线程)。创建一个新线程之后程序总共就有两个线程了。
我想此时您至少有两个重要问题。第一个问题,新线程创建之后主线程如何运行。答案,主线程按顺序继续执行下一行程序(本例中执行 "if (pthread_join(...))")。第二个问题,新线程结束时如何处理。答案,新线程先停止,然后作为其清理过程的一部分,等待与另一个线程合并或“连接”。
现在,来看一下 pthread_join()。正如 pthread_create() 将一个线程拆分为两个, pthread_join() 将两个线程合并为一个线程。pthread_join() 的第一个参数是 tid mythread。第二个参数是指向 void 指针的指针。如果 void 指针不为 NULL,pthread_join 将线程的 void * 返回值放置在指定的位置上。由于我们不必理会 thread_function() 的返回值,所以将其设为 NULL.
您会注意到 thread_function() 花了 20 秒才完成。在 thread_function() 结束很久之前,主线程就已经调用了 pthread_join()。如果发生这种情况,主线程将中断(转向睡眠)然后等待 thread_function() 完成。当 thread_function() 完成后, pthread_join() 将返回。这时程序又只有一个主线程。当程序退出时,所有新线程已经使用 pthread_join() 合并了。这就是应该如何处理在程序中创建的每个新线程的过程。如果没有合并一个新线程,则它仍然对系统的最大线程数限制不利。这意味着如果未对线程做正确的清理,最终会导致 pthread_create() 调用失败。
如果使用过 fork() 系统调用,可能熟悉父进程和子进程的概念。当用 fork() 创建另一个新进程时,新进程是子进程,原始进程是父进程。这创建了可能非常有用的层次关系,尤其是等待子进程终止时。例如,waitpid() 函数让当前进程等待所有子进程终止。waitpid() 用来在父进程中实现简单的清理过程。
而 POSIX 线程就更有意思。您可能已经注意到我一直有意避免使用“父线程”和“子线程”的说法。这是因为 POSIX 线程中不存在这种层次关系。虽然主线程可以创建一个新线程,新线程可以创建另一个新线程,POSIX 线程标准将它们视为等同的层次。所以等待子线程退出的概念在这里没有意义。POSIX 线程标准不记录任何“家族”信息。缺少家族信息有一个主要含意:如果要等待一个线程终止,就必须将线程的 tid 传递给 pthread_join()。线程库无法为您断定 tid。
对大多数开发者来说这不是个好消息,因为这会使有多个线程的程序复杂化。不过不要为此担忧。POSIX 线程标准提供了有效地管理多个线程所需要的所有工具。实际上,没有父/子关系这一事实却为在程序中使用线程开辟了更创造性的方法。例如,如果有一个线程称为线程 1,线程 1 创建了称为线程 2 的线程,则线程 1 自己没有必要调用 pthread_join() 来合并线程 2,程序中其它任一线程都可以做到。当编写大量使用线程的代码时,这就可能允许发生有趣的事情。例如,可以创建一个包含所有已停止线程的全局“死线程列表”,然后让一个专门的清理线程专等停止的线程加到列表中。这个清理线程调用 pthread_join() 将刚停止的线程与自己合并。现在,仅用一个线程就巧妙和有效地处理了全部清理。
现在我们来看一些代码,这些代码做了一些意想不到的事情。thread2.c 的代码如下:
#include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> int myglobal; void *thread_function(void *arg) { int i,j; for ( i=0; i<20; i++) { j=myglobal; j=j+1; printf("."); fflush(stdout); sleep(1); myglobal=j; } return NULL; } int main(void) { pthread_t mythread; int i; if ( pthread_create( &mythread, NULL, thread_function, NULL) ) { printf("error creating thread."); abort(); } for ( i=0; i<20; i++) { myglobal=myglobal+1; printf("o"); fflush(stdout); sleep(1); } if ( pthread_join ( mythread, NULL ) ) { printf("error joining thread."); abort(); } printf("\nmyglobal equals %d\n",myglobal); exit(0); }
如同第一个程序,这个程序创建一个新线程。主线程和新线程都将全局变量 myglobal 加一 20 次。但是程序本身产生了某些意想不到的结果。
编译代码请输入:
$ gcc thread2.c -o thread2 -lpthread
运行请输入:
$ ./thread2
输出:
$ ./thread2 ..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o myglobal equals 21
非常意外吧!因为 myglobal 从零开始,主线程和新线程各自对其进行了 20 次加一, 程序结束时 myglobal 值应当等于 40。由于 myglobal 输出结果为 21,这其中肯定有问题。但是究竟是什么呢?
放弃吗?好,让我来解释是怎么一回事。首先查看函数 thread_function()。注意如何将 myglobal 复制到局部变量 "j" 了吗? 接着将 j 加一, 再睡眠一秒,然后到这时才将新的 j 值复制到 myglobal?这就是关键所在。设想一下,如果主线程就在新线程将 myglobal 值复制给 j 后 立即将 myglobal 加一,会发生什么?当 thread_function() 将 j 的值写回 myglobal 时,就覆盖了主线程所做的修改。
当编写线程程序时,应避免产生这种无用的副作用,否则只会浪费时间(当然,除了编写关于 POSIX 线程的文章时有用)。那么,如何才能排除这种问题呢?
由于是将 myglobal 复制给 j 并且等了一秒之后才写回时产生问题,可以尝试避免使用临时局部变量并直接将 myglobal 加一。虽然这种解决方案对这个特定例子适用,但它还是不正确。如果我们对 myglobal 进行相对复杂的数学运算,而不是简单的加一,这种方法就会失效。但是为什么呢?
要理解这个问题,必须记住线程是并发运行的。即使在单处理器系统上运行(内核利用时间分片模拟多任务)也是可以的,从程序员的角度,想像两个线程是同时执行的。thread2.c 出现问题是因为 thread_function() 依赖以下论据:在 myglobal 加一之前的大约一秒钟期间不会修改 myglobal。需要有些途径让一个线程在对 myglobal 做更改时通知其它线程“不要靠近”。我将在下一篇文章中讲解如何做到这一点。到时候见。
- 参阅
Linux threads
中的文档,Sean Walton, KB7rfa
- 在
An
Introduction to Pthreads-Tcl
中,查看对 Tcl
的更改以使其能够使用 POSIX 线程
- 使用友好的 Linux pthread 在线帮助 ("man -k pthread")
- 参考
POSIX
and DCE threads for Linux
主页
- 查看
The
LinuxThreads Library
-
Proolix
,一种简单遵从
POSIX 标准的操作系统,用于 i8086+,一直在开发中
- 阅读 David R. Butenhof 的著作
Programming with POSIX
Threads
,书中讨论了许多问题,其中谈到不使用互斥对象是可能出现的种种情况
|
Daniel Robbins 居住在新墨西哥州的 Albuquerque。他是 Gentoo Technologies, Inc. 的总裁兼 CEO, Gentoo 项目的总设计师,多本 MacMillan 出版书籍的作者,包括: Caldera OpenLinux Unleashed 、 SuSE Linux Unleashed 和 Samba Unleashed 。Daniel 自小学二年级起就与计算机结下不解之缘,那时他首先接触的是 Logo 程序语言,并沉溺于 Pac-Man 游戏中。这也许就是他至今仍担任 SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻子 Mary 和刚出生的女儿 Hadassah 一起共渡时光。可通过 drobbins@gentoo.org 与 Daniel Robbins 取得联系。 |
发表评论
-
关键字auto、static、register、const、volatile 、extern
2009-07-08 15:03 1665关键字auto、static、regist ... -
C语言传值操作
2009-06-30 20:56 1515C语言传值操作 C语言中的函数参数传递都是传 ... -
C语言中的运算符及其优先级
2009-06-30 20:05 1324C语言中的运算符及其优先级 优先级 ... -
POSIX 线程详解(3)
2009-06-29 17:33 1034POSIX 线程详解(3) —使 ... -
POSIX 线程详解(2)
2009-06-29 17:16 1485POSIX 线程详解(2) ——称作互斥对象的小玩意 ... -
使用 getopt() 进行命令行处理
2009-06-16 11:32 763使用 getopt() 进行命令行处理 2006 年 5 ... -
getopt函数和getopt_long函数学习笔记
2009-06-16 11:31 1396getopt函数和getopt_long函数学习笔记 ... -
Linux命令行程序设计
2009-06-16 11:09 1001Linux命令行程序设计 ... -
Linux选项解释-getopt和getopt_long函数
2009-06-16 10:04 3576Linux选项解释-getopt和getopt_long函数 ...
相关推荐
### Posix线程详解 #### 1. 线程与进程的概念对比 在计算机科学领域,线程和进程都是实现程序并发执行的基本单位,但它们之间存在显著的区别。线程,尤其是POSIX线程(Portable Operating System Interface ...
文档“通用线程:POSIX线程详解1.doc”可能介绍了线程的创建,使用`pthread_create()`函数。该函数接受一个线程ID的指针、线程属性(可选)、线程启动函数的指针以及传递给该函数的参数。一旦创建,新线程就会开始...
以下是一个简单的POSIX线程示例程序(thread1.c): ```c #include #include #include void *thread_function(void *arg) { int i; for (i = 0; i ; i++) { printf("Thread says hi!\n"); sleep(1); } ...
POSIX(可移植操作系统接口)线程是提高代码响应和性能的有力手段。在本系列中,Daniel Robbins 向您精确地展示在编程中如何使用线程。其中还涉及大量幕后细节,读完本系列文章,您完全可以运用 POSIX 线程创建多...
POSIX线程详解[收集].pdf
POSIX线程,也称为pthreads,是操作系统中的一种线程接口,被广泛应用于包括C++在内的多种编程语言。线程允许程序同时执行多个任务,是并发编程的基础。线程和进程之间的主要区别在于,线程共享同一内存空间,而进程...
1. **线程的创建与共享内存**:在POSIX线程中,`pthread_create()`函数用于创建新线程。与fork()创建进程不同,线程创建的开销较小,因为它不需要复制整个进程的内存空间。所有线程都可以访问相同的全局变量和数据...
这是将网上收集的IBM线程编程资料整理成了一个chm文件,便于使用。内容包括UNIX线程编程指南和详解部分,适合学习POSIX线程编程技术的人员使用。
POSIX 线程详解,来自IBM,自己整理成.doc文件,感谢下载!!!!!!!!!!!!!!
POSIX线程,通常被称为pthreads,是操作系统接口的一个部分,尤其在Unix和类Unix系统如Linux中广泛使用。它是Portable Operating System Interface (POSIX)标准的一部分,为多线程编程提供了一种跨平台的方式。在...
### Posix线程编程指南:线程创建与取消详解 #### 一、线程与进程的概念及区别 在深入探讨Posix线程编程之前,理解线程与进程的基本概念及其区别至关重要。线程,作为执行体的一个更轻量级单位,能够在同一进程中...
POSIX线程,也称为pthreads,是一种标准的线程接口,被广泛应用于各种操作系统,如Unix、Linux和macOS等。线程是操作系统中的一种并行执行单元,允许一个进程中多个执行流同时运行,共享相同的内存空间。这种机制极...
### POSIX线程详解:深入理解并行编程的关键 #### 知识点一:POSIX线程的概念与背景 POSIX线程,简称Pthreads,是一种标准化的线程接口,广泛应用于Unix-like系统如Linux中,用于实现并行计算。线程作为程序执行的...
### POSIX线程编程指南知识点详解 #### 一、线程的创建与取消 ##### 一、线程创建 ###### 1.1 线程与进程 线程是一种比进程更细粒度的执行单元,它允许在同一个进程中并发执行多个代码路径。线程与进程之间的...
在Unix系统和许多类Unix系统中,POSIX线程(POSIX Threads),通常称为Pthreads,是一种使用线程的编程接口,它允许程序设计者在C和C++这样的编程语言中实现多线程。使用POSIX线程库进行多线程编程,可以让开发者...
### Posix线程函数实例详解 #### 一、线程基本概念与函数 在多线程编程中,POSIX(可移植操作系统接口)线程库是实现多线程的重要工具之一,广泛应用于类Unix系统中。为了更好地理解和使用POSIX线程库,本章节将...
标题:C++多线程详解 描述:本文深入探讨了C++中多线程的实现与管理,通过一系列实例和理论解析,旨在帮助读者全面掌握C++多线程编程技术。 知识点: ### 1. 多线程概念 在计算机程序设计中,多线程是一种允许多...
### Posix多线程编程详解 #### 一、线程概念与优势 ##### 1.1 什么是线程? 在计算机程序中,线程是指进程内的一个执行单元或控制流。简单来说,线程是在一个进程中同时执行的多个任务路径。在传统的Unix进程中,...