线程与进程区别文档格式.docx
《线程与进程区别文档格式.docx》由会员分享,可在线阅读,更多相关《线程与进程区别文档格式.docx(9页珍藏版)》请在冰豆网上搜索。
对于大多数情形,IPC不是对于代码的“自然”扩展。
通常极大地增加了程序的复杂性。
双重坏事:
开销和复杂性都非好事。
如果曾经为了支持IPC而对程序大动干戈过,那么您就会真正欣赏线程提供的简单共享内存机制。
由于所有的线程都驻留在同一内存空间,POSIX线程无需进行开销大而复杂的长距离调用。
只要利用简单的同步机制,程序中所有的线程都可以读取和修改已有的数据结构。
而无需将数据经由文件描述符转储或挤入紧窄的共享内存空间。
仅此一个原因,就足以让您考虑应该采用单进程/多线程模式而非多进程/单线程模式。
回页首
线程是快捷的
不仅如此。
线程同样还是非常快捷的。
与标准fork()相比,线程带来的开销很小。
内核无需单独复制进程的内存空间或文件描述符等等。
这就节省了大量的CPU时间,使得线程创建比新进程创建快上十到一百倍。
因为这一点,可以大量使用线程而无需太过于担心带来的CPU或内存不足。
使用fork()时导致的大量CPU占用也不复存在。
这表示只要在程序中有意义,通常就可以创建线程。
当然,和进程一样,线程将利用多CPU。
如果软件是针对多处理器系统设计的,这就真的是一大特性(如果软件是开放源码,则最终可能在不少平台上运行)。
特定类型线程程序(尤其是CPU密集型程序)的性能将随系统中处理器的数目几乎线性地提高。
如果正在编写CPU非常密集型的程序,则绝对想设法在代码中使用多线程。
一旦掌握了线程编码,无需使用繁琐的IPC和其它复杂的通信机制,就能够以全新和创造性的方法解决编码难题。
所有这些特性配合在一起使得多线程编程更有趣、快速和灵活。
线程是可移植的
如果熟悉Linux编程,就有可能知道__clone()系统调用。
__clone()类似于fork(),同时也有许多线程的特性。
例如,使用__clone(),新的子进程可以有选择地共享父进程的执行环境(内存空间,文件描述符等)。
这是好的一面。
但__clone()也有不足之处。
正如__clone()在线帮助指出:
“__clone调用是特定于Linux平台的,不适用于实现可移植的程序。
欲编写线程化应用程序(多线程控制同一内存空间),最好使用实现POSIX1003.1c线程API的库,例如Linux-Threads库。
参阅pthread_create(3thr)。
”
虽然__clone()有线程的许多特性,但它是不可移植的。
当然这并不意味着代码中不能使用它。
但在软件中考虑使用__clone()时应当权衡这一事实。
值得庆幸的是,正如__clone()在线帮助指出,有一种更好的替代方案:
POSIX线程。
如果想编写可移植的多线程代码,代码可运行于Solaris、FreeBSD、Linux和其它平台,POSIX线程是一种当然之选。
第一个线程
下面是一个POSIX线程的简单示例程序:
thread1.c
#include<
pthread.h>
stdlib.h>
unistd.h>
void*thread_function(void*arg){
inti;
for(i=0;
i<
20;
i++){
printf("
Threadsayshi!
\n"
);
sleep
(1);
}
returnNULL;
intmain(void){
pthread_tmythread;
if(pthread_create(&
mythread,NULL,thread_function,NULL)){
errorcreatingthread."
abort();
if(pthread_join(mythread,NULL)){
errorjoiningthread."
exit(0);
要编译这个程序,只需先将程序存为thread1.c,然后输入:
$gccthread1.c-othread1-lpthread
运行则输入:
$./thread1
理解thread1.c
thread1.c是一个非常简单的线程程序。
虽然它没有实现什么有用的功能,但可以帮助理解线程的运行机制。
下面,我们一步一步地了解这个程序是干什么的。
main()中声明了变量mythread,类型是pthread_t。
pthread_t类型在pthread.h中定义,通常称为“线程id”(缩写为"
tid"
)。
可以认为它是一种线程句柄。
mythread声明后(记住mythread只是一个"
,或是将要创建的线程的句柄),调用pthread_create函数创建一个真实活动的线程。
不要因为pthread_create()在"
if"
语句内而受其迷惑。
由于pthread_create()执行成功时返回零而失败时则返回非零值,将pthread_create()函数调用放在if()语句中只是为了方便地检测失败的调用。
让我们查看一下pthread_create参数。
第一个参数&
mythread是指向mythread的指针。
第二个参数当前为NULL,可用来定义线程的某些属性。
由于缺省的线程属性是适用的,只需将该参数设为NULL。
第三个参数是新线程启动时调用的函数名。
本例中,函数名为thread_function()。
当thread_function()返回时,新线程将终止。
本例中,线程函数没有实现大的功能。
它仅将"
"
输出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()的第一个参数是tidmythread。
第二个参数是指向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的代码如下:
thread2.c
stdio.h>
intmyglobal;
inti,j;
j=myglobal;
j=j+1;
."
fflush(stdout);
myglobal=j;
myglobal=myglobal+1;
o"
\nmyglobalequals%d\n"
myglobal);
理解thread2.c
如同第一个程序,这个程序创建一个新线程。
主线程和新线程都将全局变量myglobal加一20次。
但是程序本身产生了某些意想不到的结果。
编译代码请输入:
$gccthread2.c-othread2-lpthread
运行请输入:
$./thread2
输出:
..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o
myglobalequals21
非常意外吧!
因为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做更改时通知其它线程“不要靠近”。
我将在下一篇文章中讲解如何做到这一点。
到时候见。
参考资料
您可以参阅本文在developerWorks全球站点上的英文原文.
参阅Linuxthreads中的文档,SeanWalton,KB7rfa
在AnIntroductiontoPthreads-Tcl中,查看对Tcl的更改以使其能够使用POSIX线程
使用友好的Linuxpthread在线帮助("
man-kpthread"
)
参考POSIXandDCEthreadsforLinux主页
查看TheLinuxThreadsLibrary
Proolix,一种简单遵从POSIX标准的操作系统,用于i8086+,一直在开发中
阅读DavidR.Butenhof的著作ProgrammingwithPOSIXThreads,书中讨论了许多问题,其中谈到不使用互斥对象是可能出现的种种情况
查阅W.RichardStevens的著作UNIXNetworkProgramming:
NetworkAPIs:
SocketsandXTI,Volume1
关于作者
DanielRobbins居住在新墨西哥州的Albuquerque。
他是GentooTechnologies,Inc.的总裁兼CEO,Gentoo项目的总设计师,多本MacMillan出版书籍的作者,包括:
CalderaOpenLinuxUnleashed、SuSELinuxUnleashed和SambaUnleashed。
Daniel自小学二年级起就与计算机结下不解之缘,那时他首先接触的是Logo程序语言,并沉溺于Pac-Man游戏中。
这也许就是他至今仍担任SONYElectronicPublishing/Psygnosis的首席图形设计师的原因所在。
Daniel喜欢与妻子Mary和刚出生的女儿Hadassah一起共渡时光。
可通过drobbins@gentoo.org与DanielRobbins取得联系。