POSIX线程.docx

上传人:b****5 文档编号:5232128 上传时间:2022-12-14 格式:DOCX 页数:32 大小:70.02KB
下载 相关 举报
POSIX线程.docx_第1页
第1页 / 共32页
POSIX线程.docx_第2页
第2页 / 共32页
POSIX线程.docx_第3页
第3页 / 共32页
POSIX线程.docx_第4页
第4页 / 共32页
POSIX线程.docx_第5页
第5页 / 共32页
点击查看更多>>
下载资源
资源描述

POSIX线程.docx

《POSIX线程.docx》由会员分享,可在线阅读,更多相关《POSIX线程.docx(32页珍藏版)》请在冰豆网上搜索。

POSIX线程.docx

POSIX线程

POSIX线程详解

一种支持内存共享的简捷工具

DanielRobbins (drobbins@gentoo.org),总裁/CEO,GentooTechnologies,Inc.

简介:

 POSIX(可移植操作系统接口)线程是提高代码响应和性能的有力手段。

在本系列中,DanielRobbins向您精确地展示在编程中如何使用线程。

其中还涉及大量幕后细节,读完本系列文章,您完全可以运用POSIX线程创建多线程程序。

发布日期:

 2000年7月01日 

级别:

 初级 

访问情况:

 31933次浏览 

评论:

 2 (查看 | 添加评论 -登录)

 平均分(113个评分)

为本文评分

线程是有趣的

了解如何正确运用线程是每一个优秀程序员必备的素质。

线程类似于进程。

如同进程,线程由内核按时间分片进行管理。

在单处理器系统中,内核使用时间分片来模拟线程的并发执行,这种方式和进程的相同。

而在多处理器系统中,如同多个进程,线程实际上一样可以并发执行。

那么为什么对于大多数合作性任务,多线程比多个独立的进程更优越呢?

这是因为,线程共享相同的内存空间。

不同的线程可以存取内存中的同一个变量。

所以,程序中的所有线程都可以读或写声明过的全局变量。

如果曾用fork()编写过重要代码,就会认识到这个工具的重要性。

为什么呢?

虽然fork()允许创建多个进程,但它还会带来以下通信问题:

如何让多个进程相互通信,这里每个进程都有各自独立的内存空间。

对这个问题没有一个简单的答案。

虽然有许多不同种类的本地IPC(进程间通信),但它们都遇到两个重要障碍:

∙强加了某种形式的额外内核开销,从而降低性能。

∙对于大多数情形,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

#include

#include

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)){

printf("errorcreatingthread.");

abort();

}

if(pthread_join(mythread,NULL)){

printf("errorjoiningthread.");

abort();

}

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只是一个"tid",或是将要创建的线程的句柄),调用pthread_create函数创建一个真实活动的线程。

不要因为pthread_create()在"if"语句内而受其迷惑。

由于pthread_create()执行成功时返回零而失败时则返回非零值,将pthread_create()函数调用放在if()语句中只是为了方便地检测失败的调用。

让我们查看一下pthread_create参数。

第一个参数&mythread是指向mythread的指针。

第二个参数当前为NULL,可用来定义线程的某些属性。

由于缺省的线程属性是适用的,只需将该参数设为NULL。

第三个参数是新线程启动时调用的函数名。

本例中,函数名为thread_function()。

当thread_function()返回时,新线程将终止。

本例中,线程函数没有实现大的功能。

它仅将"Threadsayshi!

"输出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

#include

#include

#include

#include

intmyglobal;

void*thread_function(void*arg){

inti,j;

for(i=0;i<20;i++){

j=myglobal;

j=j+1;

printf(".");

fflush(stdout);

sleep

(1);

myglobal=j;

}

returnNULL;

}

intmain(void){

pthread_tmythread;

inti;

if(pthread_create(&mythread,NULL,thread_function,NULL)){

printf("errorcreatingthread.");

abort();

}

for(i=0;i<20;i++){

myglobal=myglobal+1;

printf("o");

fflush(stdout);

sleep

(1);

}

if(pthread_join(mythread,NULL)){

printf("errorjoiningthread.");

abort();

}

printf("\nmyglobalequals%d\n",myglobal);

exit(0);

}

回页首

理解thread2.c

如同第一个程序,这个程序创建一个新线程。

主线程和新线程都将全局变量myglobal加一20次。

但是程序本身产生了某些意想不到的结果。

编译代码请输入:

$gccthread2.c-othread2-lpthread

运行请输入:

$./thread2

输出:

$./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做更改时通知其它线程“不要靠近”。

我将在下一篇文章中讲解如何做到这一点。

到时候见。

通用线程:

POSIX线程详解,第2部分

称作互斥对象的小玩意

DanielRobbins (drobbins@gentoo.org),总裁/CEO,GentooTechnologies,Inc.

简介:

 POSIX线程是提高代码响应和性能的有力手段。

在此三部分系列文章的第二篇中,DanielRobbins将说明,如何使用被称为互斥对象的灵巧小玩意,来保护线程代码中共享数据结构的完整性。

发布日期:

 2000年8月01日 

级别:

 初级 

访问情况:

 17288次浏览 

评论:

 0 (查看 | 添加评论 -登录)

 平均分(37个评分)

为本文评分

互斥我吧!

在 前一篇文章中 ,谈到了会导致异常结果的线程代码。

两个线程分别对同一个全局变量进行了二十次加一。

变量的值最后应该是40,但最终值却是21。

这是怎么回事呢?

因为一个线程不停地“取消”了另一个线程执行的加一操作,所以产生这个问题。

现在让我们来查看改正后的代码,它使用 互斥对象(mutex)来解决该问题:

thread3.c

#include

#include

#include

#include

intmyglobal;

pthread_mutex_tmymutex=PTHREAD_MUTEX_INITIALIZER;

void*thread_function(void*arg){

inti,j;

for(i=0;i<20;i++){

pthread_mutex_lock(&mymutex);

j=myglobal;

j=j+1;

printf(".");

fflush(stdout);

sleep

(1);

myglobal=j;

pthread_mutex_unlock(&mymutex);

}

returnNULL;

}

intmain(void){

pthread_tmythread;

inti;

if(pthread_create(&mythread,NULL,thread_function,NULL)){

printf("errorcreatingthread.");

abort();

}

for(i=0;i<20;i++){

pthread_mutex_lock(&mymutex);

myglobal=myglobal+1;

pthread_mutex_unlock(&mymutex);

printf("o");

fflush(stdout);

sleep

(1);

}

if(pthread_join(mythread,NULL)){

printf("errorjoiningthread.");

abort();

}

printf("\nmyglobalequals%d\n",myglobal);

exit(0);

}

回页首

解读一下

如果将这段代码与 前一篇文章 中给出的版本作一个比较,就会注意到增加了pthread_mutex_lock()和pthread_mutex_unlock()函数调用。

在线程程序中这些调用执行了不可或缺的功能。

他们提供了一种 相互排斥的方法(互斥对象即由此得名)。

两个线程不能同时对同一个互斥对象加锁。

互斥对象是这样工作的。

如果线程a试图锁定一个互斥对象,而此时线程b已锁定了同一个互斥对象时,线程a就将进入睡眠状态。

一旦线程b释放了互斥对象(通过pthread_mutex_unlock()调用),线程a就能够锁定这个互斥对象(换句话说,线程a就将从pthread_mutex_lock()函数调用中返回,同时互斥对象被锁定)。

同样地,当线程a正锁定互斥对象时,如果线程c试图锁定互斥对象的话,线程c也将临时进入睡眠状态。

对已锁定的互斥对象上调用pthread_mutex_lock()的所有线程都将进入睡眠状态,这些睡眠的线程将“排队”访问这个互斥对象。

通常使用pthread_mutex_lock()和pthread_mutex_unlock()来保护数据结构。

这就是说,通过线程的锁定和解锁,对于某一数据结构,确保某一时刻只能有一个线程能够访问它。

可以推测到,当线程试图锁定一个未加锁的互斥对象时,POSIX线程库将同意锁定,而不会使线程进入睡眠状态。

请看这幅轻松的漫画,四个小精灵重现了最近一次pthread_mutex_lock()调用的一个场面。

 

图中,锁定了互斥对象的线程能够存取复杂的数据结构,而不必担心同时会有其它线程干扰。

那个数据结构实际上是“冻结”了,直到互斥对象被解锁为止。

pthread_mutex_lock()和pthread_mutex_unlock()函数调用,如同“在施工中”标志一样,将正在修改和读取的某一特定共享数据包围起来。

这两个函数调用的作用就是警告其它线程,要它们继续睡眠并等待轮到它们对互斥对象加锁。

当然,除非在 每个 对特定数据结构进行读写操作的语句前后,都分别放上pthread_mutex_lock()和pthread_mutext_unlock()调用,才会出现这种情况。

回页首

为什么要用互斥对象?

听上去很有趣,但究竟为什么要让线程睡眠呢?

要知道,线程的主要优点不就是其具有独立工作、更多的时候是同时工作的能力吗?

是的,确实是这样。

然而,每个重要的线程程序都需要使用某些互斥对象。

让我们再看一下示例程序以便理解原因所在。

请看thread_function(),循环中一开始就锁定了互斥对象,最后才将它解锁。

在这个示例程序中,mymutex用来保护myglobal的值。

仔细查看thread_function(),加一代码把myglobal复制到一个局部变量,对局部变量加一,睡眠一秒钟,在这之后才把局部变量的值传回给myglobal。

不使用互斥对象时,即使主线程在thread_function()线程睡眠一秒钟期间内对myglobal加一,thread_function()苏醒后也会覆盖主线程所加的值。

使用互斥对象能够保证这种情形不会发生。

(您也许会想到,我增加了一秒钟延迟以触发不正确的结果。

把局部变量的值赋给myglobal之前,实际上没有什么真正理由要求thread_function()睡眠一秒钟。

)使用互斥对象的新程序产生了期望的结果:

$./thread3

o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooo

myglobalequals40

为了进一步探索这个极为重要的概念,让我们看一看程序中进行加一操作的代码:

thread_function()加一代码:

j=myglobal;

j=j+1;

printf(".");

fflush(stdout);

sleep

(1);

myglobal=j;

主线程加一代码:

myglobal=myglobal+1;

如果代码是位于单线程程序中,可

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 艺术

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1