Chinaunix首页 | 论坛 | 博客
  • 博客访问: 129108
  • 博文数量: 30
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 338
  • 用 户 组: 普通用户
  • 注册时间: 2014-02-19 17:33
文章分类
文章存档

2017年(2)

2014年(28)

我的朋友

分类: Android平台

2014-02-27 13:47:54

线程

学了那么多有关进程的东西,一个作业从一个进程开始,如果你需要执行其他的东西你可以添加一些进程,进程之间可以通信、同步、异步。似乎所有的事情都可以做了。

对的,进程是当初面向执行任务而开发出来的,每个进程代表着一个动作,你可以说一个进程组代表一个任务,或者一个会话代表一个任务,关键是你的任务就是在进程的执行与进程之间的交互中被完成。

但是,我们知道,进程在操作系统中被设计成独立的个体,进程与进程之间有绝对的界限,他们的通信是需要通过内核的。这个耗费是蛮大的,并且,进程对资源的占有也时常是一个让进程编程负担的原因。

总的来说,如果你希望系统中并行的运行的一些任务,你就需要跑多个进程,而进程它跑动的前提是对资源的占有,它是一个独立体,从某种意义上讲,它的执行不依赖其他的东西(当然内核的支持肯定是要的)。这种设计风格,保证了进程的独立性,但是也增加了进程的重量。其次,不管进程独立风格怎么样,进程之间的通信始终是需要的。这种通信需要内核的支持,这也是不够理想的。

基于上面的问题,线程的概念被提出来了。

没有线程概念之前,进程被看做是一个执行与资源的最小占有体。进程被看做执行的一个最小体,什么意思呢,一个进程就是用来服务一个请求的。多个请求需要创建多个进程来服务请求。关键是,如果这些请求是类似的、关联非常高的,这个时候,每个进程对资源的占有,以及进程之间的通信就非常频繁。进程显然不适合这种模式。

线程,抽象的讲就是将进程这个作为执行的个体拆分成若干个小执行体,这些小的执行体共享一个进程所占有的资源。

如果一个进程,是一个手术室,那么该手术室需要对很多工具进行独占,并且保持一定的独立性(不独立将会很危险,想想,不同的手术室,经常为某个手术工具而等待,将会很恐怖)。手术间的通信也会经过外面,而不是直接的。

而一个手术室内部呢?有若干个医生、护士,他们各司其职,工具共享、交流通畅。这就是进程和线程的区别。

不过从另外一个角度上讲,线程和进程只是从不同的角度(粗细度)看操作系统的执行体。就线程本身而言,它的最大特点就是共享一个进程的几乎所有资源(内存空间等),但是作为一个执行体,它也拥有与进程类似的相关控制管理操作(创建、退出、等待、通信)。所以,认清进程与线程的区别的基础上,看到他们的相同点,是学习两者的好方法。

线程基本管理

创建线程

/usr/include/bits/pthread.h

Extern int pthread_create( pthread_t * __restrict__new thread,

__constpthread_attr_t*__restrict__attr,

Void * (* __start _routine ) (void *),

Void *__restrict __arg)

/usr/include/bits/pthreadtypes.h

Typedef unsigned long int pthread_t;

可以看到,创建一个新的线程,需要注入一段“执行脚本”,就像创建一个新的进程一样,复制父进程的“执行脚本”,或者使用exec函数从文件系统中调入一个新的执行文件进来。进程是独立体,自然“执行脚本”也是独立体。而线程被定义为进程执行体的一部分,所以,线程的“执行脚本”自然是进程“执行脚本”的一个部分。

可知,上述所说的执行脚本,在编写程序的时候,就是程序本身,程序内部最能代表一个相对独立的执行体的东西,自然是函数。所以,新建一个线程的所需要的东西就是指定一个该进程(code)内部的函数作为该线程的执行脚本。从这点我们也可以看出,如果没有线程的概念,进程就像是一个按照“执行脚本”(code)顺序执行的执行体,线程的作用是从这个执行体中抠出一个相对独立的部分来执行。有一定的异步性。从上层来讲,一个进程仍然是按照用户编程的程序进行跑着。但是线程似乎让这个“执行体”并发的进行起来。

继续使用手术室的例子,手术的过程就是一段手术的可执行程序中的code所指示的。每个线程,取该code中的某个部分做该做的事。主刀者做它的事,输血者做输血的事,观察者看仪器。他们各司其职、但也并是不保持绝对的独立。

可以说,添加了线程概念的进程,在执行的时候,有一定的异步特点。

线程退出

与进程的exitabort对应。一个线程在该线程内部突出的方式有:

/usr/include/bits/pthreadtypes.h

Extern void pthread_exit (void * _retval);

线程退出的情景有:

1) 调用pthread_exit

2) 调用pthread_cancel

3) 所属进程退出

4) 线程函数结束

5) 其中的一个线程执行exec函数

要知道,exec函数的意思是唤起一个新的可执行程序,来替换现有进程,这是一个进程级别的命令的,但是,在某个线程中执行,所以一个线程执行这个命令会导致其它的所有线程全部被退出。

等待线程

进程中,父进程可以使用waitwaitpid等待子进程的执行,线程中也是一样:

Extern int pthread_join (pthread_t__th, void**__thread_return);

独立线程

使用等待线程,必须指定某个线程的id,必须是关联的线程(自然是属于一个进程的),一个进程内部的线程默认是与进程关联的,可以通过函数执行,让该进程不关联。

Extern int pthread_detach (pthread_t__th);

线程独占的资源

这里我们讲的线程独占资源,是指那些线程退出的时候会释放的资源,其他的线程是无法访问的线程。其实这个问题我们只要知道,线程是进程中某个函数的异步执行而已,就很容易推到出来。

一个进程的内存空间中,有code区、数据区、BCC区,这三个区是可执行程序在还未进入内存的时候就已经分配好了的,所以,它是一个进程的固有区,进入内存后,再加上栈区和堆区。线程中有可能申请了全局或者静态数据,这些数据都会在数据区或者BCC区中,线程中同时会有可能申请堆上空间,这个在一个进程内部,是通用的,因为该区的数据只受申请很释放函数管理,与是哪个线程申请,哪个释放无关。在一个进程内部它还真的无所谓被谁申请和释放。

所以在一个进程内部,线程所独占的,也就是局部变量,其实一个函数的角度去看,局部变量的确只属于该函数,程序的其它的部分根本不知道它的存在。

如果没有线程概念的时候,这些局部变量被压入在统一的栈中,但是,有了线程,这种方式有不通用了,我们知道“执行脚本”执行的最根本依靠就是“栈的”先入后出方式。不同的线程之间是异步的,所以他们不可能在共享一个栈(如果是同步的,仍然是可以的)。

解决的方法可以想到,在没有线程的概念中,所以的过程都在栈中发生,一个函数的过程局部变量在栈的某个部分相对其他事物是独立了,也就是说,其他部分仅会依赖你的开始和结束,中间部分他们看不到。所以这个就可以独立出来。

所以线程是可以独立拥有自己的栈的。当然线程除了拥有自己独立的栈,还拥有其他独立的东西。

线程退出前的动作

与对于资源的占有,本该属于进程的动作,进程退出它所占有的所有资源都会被释放,所以进程在退出的时候无需太多考虑资源的释放问题(可能,有什么对某个资源的不同进程之间的竞争,需要进程内部协调)。

但是在一个进程内部,线程之间也会出现资源的竞争,这不要与线程共享进程所占有的资源这个概念搞混淆。从另外一个角度上讲,各个进程可以看做对系统拥有的所有资源共享呢。共享是共享,但是真正要使用的时候,有些工具就只能一个执行体使用。所以进程内部同样存在资源竞争问题。只是范围缩小了而已。

在进程中,由于各个线程处于一个“空间内”,有关资源竞争的控制直接由线程来做,我们设想,有一个资源记录薄,上面记录的资源全部只能是线程独的。那个线程想使用某个工具就需要到该记录簿上,打个记号,(如果已经被人使用了那只能等了)。因为线程对进程空间各项资源的共享,所以这种模式实现起来很方便。关键是,如果某个线程死掉了,但是没有在死之前到记录薄那里将那个占有记号给取消掉,那么其他的线程永远也无法使用该工具。

所以线程,在可预见性退出或者不可预见性退出的之前需要从分考虑资源释放问题。

当然,除了资源释放,我们还有很多其他的工作,需要在退出之前做的。这个动作类似于进程的atexit定义的动作。在线程中,我们将那些要求在线程退出之前的动作成为清理工作。

VoidPthread_cleanup_push( void (* routine)(void *),void*arg);

Voidpthread_cleanup_pop (int execute);

在线程中使用这两个函数来做有关的清理工作。

这中机制设计起来非常巧妙,在线程函数中,你什么时候申请了一个独占资源,你都使用Pthread_cleanup_push函数注册一个清理函数,该函数被压入栈中。也就是说,你申请了多少资源(或者其他的事情),你希望在该线程结束之前被释放(或者其他动作)。有可能有许多的动作被压入栈中。

在结束之前,通过pthread_cleanup_push逐个出栈,并执行之前设定好的函数,来进行清理工作。

之所以选择栈这个先入后出的结构,大家想想就明白了。就像为什么析构函数是与构造函数反方向释放空间一样。在申请资源的时候,只可能后面的资源(变量),依赖前面的资源。所以释放的时候按照反的方向释放才是安全的。

这两个函数的具体用法,就是在线程函数中,在需要注册清理函数的地方调用pthread_cleanup_push函数。在最后的结束的地方,逐个使用pthread_cleanup_push弹出函数执行。程序可能执行不到pthread_cleanup_push那么地方,但是系统规定,只要线程终止(不管是正常终止,还被强行终止),都会执行这个东西。

这个时候你就会发现这种清理机制的巧妙之处了。线程不管执行到那个地方(可能还有些pthread_cleanup_push函数都没有执行),都可以终止,这个时候调用那些清理函数,就是合理的。如果使用那种类似于atexit的方式,写死了在线程结束前要做的事情,那么有时候我资源都没有申请(没有执行到那个pthread_cleanup_push那个地方就被终止了),你却要调用函数来释放,是不合适的。而线程使用这种方式,保证了申请了才被释放的机制。

取消线程

线程外部发出取消的命令,是以信号的形式发送给该线程的,也就是说并不是那种直接控制的方式,因为线程已经是一个独立的执行体,所以它有权地决定是否以及如何听取你的取消命令。

Extern int thread_cancel(pthread_t__cancelthread);

Extern int pthread_setcancelstate(int __state, int *__oldstate);

连个state值,分别是:

PTHREAD_CANCEL_DISABLE

PTHREAD_CANCEL_ENABLE

Extern int pthread_setcanceltype(int __type, int *__oldtype);

两个type的有效值为:

PTHREAD_CANCEL_ASYNCHRONOUS

PTHREAD_CANCEL_DEFERRED

线程私有数据(tsd

有时候,我们希望执行体之间共享、共用某个数据,通过两个执行体对一个变量进行修改来达到通信的效果。(其实有一种方式在执行体之间传递数据就是使用参数,但是要实现两个执行体之间的这种通信效果是很不方便的)

最好的方法就是使用全局变量。

当然全局变量是一种相对的概念,只要两个执行体都认识该全局变量,应该就算是全局变量。

有了这个全局变量,我能就可以在不同的函数中实现通信,尤其是在这些函数已经独立出来(使用线程)。

例如,对进程占有的某个资源的占用,我们使用一个全局变量来指示还有多少可以用的,每次新建一个线程,可以实施对该资源变量的修改来表达它的数量的增减。

对,这种进程线程共享机制是很有用的。

但是,有时候,我希望有些东西独属于某个线程,并且可以跨越线程的几个函数。什么意思呢?进程中的code可以有很多的函数,而线程是从某个函数进入的,并且这个函数有可能访问其他的函数。现在对于同一个函数fun1,它内部调用了fun2.同时两个函数都操作了一个全局变量n。现在我从fun1函数这里开始产生出多个线程,每个线程都对这个变量n独有,这就是说这个线程对变量n的更改只限于该线程内部的那些函数,与其他线程无关。这就与全局变量不同,全局变量没有线程的差别,只要被谁改了,就被改了。

也许你会想到一个方法,使用参数值传递,将全局变量从fun1传递进入,但是这样会很不方便,因为在某个一个线程中,可能有很多的函数都会使用该参数,这就是回到了前面我们讨论的如何使用一个变量在各个执行体之间流转的问题。使用的就是全局变量。

于是我们现在需要的就是一个类似于全局变量,但是有不是全局变量的东西,它属于一个线程内部的全局变量。

这就是我们要学到的东西——线程私有数据(TSD

Pthread_key_tkey;

在编写代码时使用这样一个语句在程序中定义一个类似于全局变量的东西。

在每个函数内部使用函数:

Int pthread_key_create(pthread_key_t* key, void ( *destr_function ) (void *));

这句话的作用就是,在某个线程中,执行这个函数,将key这个全局变量注入数据,这个时候,你要注意这不是普通的在线程内部

Key=10

这个函数,会在该线程的范围内,开闭一个独立的空间来保存key的内容。在该线程的范围内,该名字(key)所引用的地址都是那个地方。

不管哪个线程调用这个函数,都会独立在线程范围内开辟那个空间。名字使用key

有些人会说,那这样与我在该函数内部从新定义一个key,然后使用有什么区别呢?有的,由于这个可以仍然扮演全局变量的角色。

当我们编写代码的时候,我们并不区分线程,我们也不知道某个函数会被唤起多少个线程,但是不同的函数之间通畅的交流使用全局变量就是一个很好的方法,又由于全局变量不能保证线程之间的独立。而TSD,在编码的时候保持着全局变量的样子和作用。在实际跑的时候,却是一个与线程相关的全局变量。

当然了,Pthread_key_t是一个特殊的全局变量。

读写那个线程全局变量的需要使用特殊的函数。

Int pthread_setspecific (pthread_key_t key, const void * pointer);

Void * pthread_getspecific(pthread_key _t key);

所以,有关线程私有数据本来就是一个比较复杂的问题,它所表达的就是一个同名不同地址的全局变量。也就是全局同名,各线程中不同地址。

其实这个东西还是满难理解的。如果是一个应用的技巧,也就是说不涉及的内核的特殊支持。那么可以这样理解。

首先,定义个全局变量pthread_key_tkey 这样的结构。

该结构自然可以被然后一个线程使用,该结构内部有一个这样的列表。

List——>pointer>

其中,tid是当前线程的线程idpointer是某个地址为void * 类型。

使用Pthread_key_create()函数意在创建一个这样的结构,开始的那个全局变量很可能是一个类似于空壳的东西,似乎就是一个指针而已。只用通过这个函数,才能从系统中得到一个这样的结果,并且顺便,注册一个函数,用于每个线程在退出的时候释放与key关联的那个空间。

Pthread_setspecific()函数,将某个地址注册到那个key结构中,使用tid作为标示。

Pthread_getspecific(),key结构中当前tid关联的指针地址返回。

内存空间如下:


在某个线程中,只需要通过set/getsepcific函数,不管在那个函数内部,在一个线程内部从key那个结构得到的指针永远是一致的。不过想来,这绕得也太大弯了。

线程属性管理

前面介绍了线程基本的概念以及有关创建、退出、等待等等基本的内容,这一节主要介绍线程的属性控制管理。

属性是一件东西的内在内容,作为一个独立的执行体,线程需要独占一些东西以支持它的独立执行。(虽然我们一直强调线程共享进程的资源)。

1) 程序计数器,由于线程是被拆分了的进程执行体,cpu中控制指令执行的东西必须被各个线程所独有。这个事执行环境的其中一个。

2) 一组寄存器,与程序计数器一起构成了线程执行基本环境。这两个属性是程序员所无法控制,也不应该被控制,就应该被透明化的东西。

3) 栈,前面分析了,栈是一个程序执行的依赖结构,线程具有一定的独立性,需要一个属于自己的独立栈来安排它的执行执行过程。这栈中的内容由用户编写的函数大体决定。

4) 线程信号掩码,设置每个线程的阻塞信号。

5) 线程局部变量,存在于栈中

6) 线程私有数据(tsd),是一种线程级别的全局变量的应用(需内核支持)。

我们将线程相关的东西分为几类,分别是,基本控制管理(创建、退出等等),内容基本管理(函数编写时确定,包括线程是有数据),属性管理(有关一个线程的栈大小、调度策略与参数、能否被等待等属性,这些属性影响着线程运行),以及程序员无法触及的内容(程序计数器、寄存器等)。

POSIX给操作系统提供的管理线程属性的结构体:

Typedefstruct__pthread_attr_s

{

Int__detachstate;//是否可被等待默认PTHREAD_CREATE_JOINABLE

Int__schedpolicy;//调用策略默认 SHED_OTHRE.

struct__sched_param__schedparm;//调用策略参数默认为优先级0

int__inheritsched;//是否继承创建者的调度策略

int__scope;//争用范围默认PTHREAD_SCOPE_PROCESS

siee_t__guardsize;//栈保护区大小

int__stackaddr_set;//

void *__stackaddr;//栈起始地址,默认为NULL,系统自行分配

size_t__stacksize;//栈大小,默认为0,系统默认栈大小

}pthread_attr_t;

上面是系统POSIX提供给我们管理线程属性的对象结构,按理说我们能够直接通过赋值控制它,但是,POSIX提供了一系列的操作函数来控制它。原因很有多的,关键是可以通过操作函数的控制来控制属性设置的合理性。

注意,线程属性对象与线程有一定的独立性,这个结构本身并不属于任何线程,只是当我们用于创建新的线程的时候,调用的那个pthread_create函数,需要给予的一个属性参数,如果给出的是NULL那就是使用系统默认的属性参数。

所以一般的过程是,在程序开始创建线程之前,系统先定义好这个线程属性对象。主要的函数有:

(这些函数,有一个固定的格式,函数名以pthread_attr_开头,返回值,大部分尊崇UNIX的管理int类型,0表示成功,-1表示失败,不管是设置还是要获取,都在函数参数列表中表现,第一个参数往往是一个线程参数对象的引用(指针),第二个参数是要设置的值(使用值传递),或者是要获取(注入的属性值),使用引用(指针传递), 并且,通常复杂参数也使用引用(指针),以防止复制该参数)

函数原型

说明

Extern intpthread_attr_init( pthread_attr_t * __attr)

按照系统默认值初始化该线程属性结构

Extern int pthread_attr_destroy( pthread_attr_t * attr)

销毁已初始化的

Extern int pthread_attr_setdetachstate( pthread_attr_t*__attr,int__detachstate)

设置可被等待属性

Extern int pthread_attr_getdatachstate( Pthread_attr_t * __attr, int*__detachstate)

获得可被等待状态

Extern intpthread_attr_setstacksize(pthread_attr_t * __attr, size_t__stacksize)

设置线程栈的大小

Extern int pthread_attr_getstacksize(pthread_attr_t * __attr,size_t*__restrict__stacksize)

获得线程栈的大小

Extern int pthread_attr_setstackaddr(pthread_attr_t* __attr, void *__stackaddr)

设置线程栈的起始地址(一般不能设置,默认值为NULL,表示让系统决定,如果设置了就只能创建一个线程了,因为很难想象两个线程共用一个栈,就像两个进程共用一个栈那样不合理)

Extern int pthread_attr_getstackaddr(pthread_attr_t * __attr, void ** __restrict__stackaddr)

获得线程栈的起始地址

Extern int pthread_attr_setguardsize(pthread_attr_t * __attr, size_t__guardsize

设置栈保护区大小(栈保护区设置需要多加考虑,不假思索的设置往往只会浪费内容空间)

Extern int pthread_attr_getguardisze(pthread_attr_t *__attr, size_t* __guardsize)

获取栈保护区大小

Externint pthread_attr_setinheritsched(pthread_attr_t* __attr, int__inherit)

设置是否从创建者那里继承调度策略和关联属性。

PTHREAD_INHERIT_SCHED

PTHREAD_EXPLICITY_SCHED(默认)

Extern int pthread_attr_getinheritsched(__constpthread_attr_t *__restrict__attr, int *__inherit)

获取是否继承调度策略。

Externint pthread_attr_setschedpolicy( pthread_attr_t * __attr, int __policy)

设置调度策略

#define SHCED_OTHER0//默认

#define SHCED_FIFO1

#defineSHCED_RR2//时间轮转

Extern int pthread _attr_getschedpolicy(__constpthread_attr_t* __restrict__attr, int * __policy)

获取调度策略

Extern int pthread_attr_setschedparam(pthread_attr_t *__attr,__conststructsched_param*_restrict__param)

设置调度参数

Structsched_param

{

Int__sched_priority;

}

Extern int pthread_attr_getschedparam(_constpthread_attr_t * __restrict__attr,structsched_param*__restrict

获取调度策略

上面列出了,通过管理线程属性对象来在线程被创建时的管理该线程属性。

在线程已经执行的时候,我们时候能够在内容内部、或者是外部得到、设置相关属性呢?当然是可以的。当然这些属性有是在线程属性对象中的,也有不在线程属性对象中的,例如能否被取消等设置。

1) Extern pthread_t pthread_self (void) 获取当前线程id(线程内部)

2) Extern int pthread_setschedprio(pthread_t__target_thread,int__prio);(线程外部)

3) Extern int pthread_setschedparam( pthread_t__target_thread,int__policy,structsched_param*__param);(线程外部)

4) Extern int pthread_getschedparam(pthread_t__target__thread, int*__policy,structsched_parm*_-param); (线程外部)

5) Extern int pthread_detach (pthread_t__t);(线程外部,改变线程是否能被等待)

6) Extern int pthread_setcancelstate(int __state, int *__oldstate); (线程内部设置能否被取消)

7) Extern int pthread_setcanceltype(int __type, int *__oldtype); (线程内部设置被取消的类型)

总的来说,设置获取一个线程的相关属性,可以通过线程属性对象在创建的时候设置的方式、也可以直接对已存在的线程进行设置,两者之间有交集。

线程间通信

作为一个执行体,线程间与进程间是一样的,同样需要通信、同步、异步。不过,线程间与进程间有许多的不同。线程共享一个进程的空间(当然自己也是保留独有的东西的),我们前面分析过,线程就是将一个顺序执行的可以执行文件,异步的执行起来,在一个进程空间中,线程遵守着进程code中的某个一段的规范,自行执行。

由于它们继承资源的共享,所以,他们之间的通信可以很好的利用进程内部的共有事物进行相关的同步、异步。虽然有些时候仍然需要操作系统内核的支持,但是就是由于线程共享进程资源这个特点,是线程之间的通信非常的轻巧、方便。这是通常选择多线程而不是多进程软件方式的一个重要原因。

进程间通信的方法主要有:

· 互斥锁:这是线程通信的基本原理和思想,保证同一时刻只能有一个线程访问某个资源。

· 条件变量:配合互斥锁,实现较好的资源互斥访问策略。

· 读写锁:更为丰富的互斥锁机制,实现对资源的读写区别资源互斥访问策略。

· 信号:进程信号在线程中的应用。

我们知道,当引入了线程的概念,就一个进程而言,它只是一个资源占有的实体而已,即使该进程只有一个线程,它也只是一个线程。当然对于进程间来说,是没有线程的概念的。但是进程间的通信,又必须由进程内部某个线程来支持。这就是出现了一个很奇怪的现象。进程间是无线程的概念的,但是进程间的通信却是有线程来完成的。(当然其实纠结这个问题基本毫无意义,一个手术室是一个进程,对外显示为一个手术室,而不是若干个人。如果手术室中的人表示不同的线程,那么手术室之间的通信仍然是两个手术室内部的人做的。只是对彼此而言,大家都认为是和手术室通信)。

信号

就信号这个概念来说,进程(当时没有提出线程的概念的时候)可以给别人发信号(kill),可以给自己发信号(raise, alarm),来实现进程间、以及进程本身的异步执行。

引入线程概念之后,我们就知道,我们所的种种都是使用那个线程来完成的,所以说,那些有关进程的相关操作(例如信号)完全是在线程中可用的。例如,在一个进程中的如何线程里,你都可以使用kill函数向别的进程发出信号,你也可以使用raise给自己发出信号,在进程中的无论哪个线程安装了该信号,就会执行。

引入线程后,在加入一些有关线程特有的信号操作,我们知道其实进程的信号操作,完全就是给线程用的,但是线程无关的。新加入的信号操作可以实现线程之间相互发信号,并且设置进程内部不同线程的信号掩码。

这两个操作都是针对线程来实现。

Extern int pthread_kill (pthread_t_threadid, int__signo);

Externintpthread_sigmask (int__how,__const__sigset_t*__restrict__newmask,

__sigest_t*__restrict__oldmask);

这个pthread_sigmask函数与pthread_sigprocmask函数对应,后者是来为整个进程进行屏蔽的,它的影响会到达每个线程,而前者则是为各个线程设置。

所以,信号,并不是独属于进程间通信的,它除了能够表达在进程间进行信号的传递,它还能够在线程间进行传递。至于其他的进程间通信机制,他们虽然仍然是通过线程操作、管理(引入线程后进程已经不再是一个执行体的代表了),但是仍然只是设计进程间的通信,无法将他们应用到线程间通信。

基于共有资源(变量)的线程间的同步、异步通信

我们总说,线程共享进程的资源,其实这里的共享,最大的部分还是对全局变量的共享。要理解,线程是某个可执行文件中的某个函数跳出来独立执行而已,它并不脱离它所属的环境,也就是说,要判断某个线程是否对资源(变量、空间、资源等,其实在程序的层面上,资源就是变量或者是变量指向的某个实体)具有访问和使用权限(或者说该资源对该线程是否可见),只要看程序就可以了,该线程所承载的函数能见到的资源,该线程就能够访问。

所以你会看到,线程间的通信手段(互斥锁、条件变量、读写锁)都是基于一个全局变量,不属于任何函数的变量,是某个高层函数中的变量。这样可以达到他们能够看见的效果。

线程对进程资源的共享,表达出来的意思是“可见”,但是不同的线程是否能够随意的操作(读写)它可见的公共资源,就是线程间同步所要做的东西。

事实上,线程间数据通信还不是很重要,因为线程间共享率很高,不像进程间。但是这种共享率过高的情况导致了另外一个问题,那就是对某个资源的访问,需要协调不同的线程的过程,要不然会出现混乱,也就是说需要管理线程对某个公共资源的操作,比如,同一时间只能有一个线程操作它(互斥锁),或者是可以有很多线程同时读,但只能有一个线程同时写(读写锁)。这就是同步,不要太纠结“同步”的同字,同步的意思是协调,规范不同执行的执行。

值得注意的是,在线程领域中,一方面,我们同步是为了线程之间能够按照某种先后过程调度来访问某个公共资源(变量);另一方面,我们实现这种同步的方式基本上也是使用公共资源(变量)的特性来实现的。

所以,线程中同步机制,起点和落点是线程的共享进程资源特性,我们使用那些不注重访问策略的共享变量来控制,那些需要控制的共享变量。

互斥锁

互斥锁,是以排它的方式控制共享数据被线程访问的过程,这种模式是通过控制线程中访问该共享数据的代码来控制他们的执行,而不是直接控制该共享数据。意思就是,线程中(函数中)的某一段代码,必须要满足某个条件才能继续往下执行,否则阻塞在那里,直到条件达到位置。

它的效果类似于:

Boolmutex;

线程1

While(!Mutex);

Mutext=false;

……

访问公共资源代码

……

Mutex=ture

线程2

与线程1类似。

两个线程,使用公共数据mutex来控制对某个重要资源的互斥访问,线程1中,一定要等到mutex的值为true时,才能突破while循环,进入下一步,否则会一直在那里执行着,直到等到线程二,执行完将mutex编程true

这种模拟虽然意思到了,但是真正的linux互斥锁,是阻塞策略。而不是循环问询。自然是阻塞策略的好,当线程1发现mutex不可用,于是就阻塞起来,知道它可用的时候系统在发送信息唤醒该线程,并是该线程获得该共享资源的控制权。这里关键的一个就是如何唤醒被阻塞的线程,这是需要内核支持的。当然为了程序员简单,那些复杂的过程,都已经被包装好了。主要的互斥锁语句如下:

1)定义互斥锁:

Pthread_mutex_tlock;//必须是一个线程公共区(一般是一个全局变量)

2)初始化互斥锁:

Externint pthread_mutex_init( pthread_mutext_t *__mutex,__constpthread_mutexattr_t *__mutexattr);

类似与使用线程属性对象创建线程一样,这里使用pthread_mutexattr_t线程互斥属性的引用来初始化该互斥锁,如果是NULL,则使用系统默认的方法。

还可以直接静态初始化互斥锁:

Pthread_mutex_tmp=PTHREAD_MUTEX_INITIALIZER;

这样就免除了调用初始化函数,其中PTHREAD_MUTEX_INITIALIZER是这么被定义的:

#define PTHREAD_MUTEX_INITIALIZER{ {0}},从这里我们依稀可以理解mutex这个结构的大体内容了。

注意:我们发现,在linux上的C编程中,总是有这样的过程,先定义,然后初始化,我们怎么看都觉得怪怪的,那是因为,如果是面向对象语言,类似于结构体这样的符合对象,是有构造函数的,而构造函数的作用就是分配并初始化符合结构。但是C中没有,所以,一个复合结构被定义后需要初始化,而初始化,如果直接使用元素赋值的方法那将是非常恐怖和不便的,所以C中使用的是定义初始化方法,这个方法的存在意义就是作为构造函数而存在的,只不过需要程序员人为定义。

3)销毁互斥锁

Externint pthread_mutex_destroy ( pthread_mutex_t*__mutex)

这里有一个对应关系,在C++中,析构函数负责对对象生命周期的收尾工作,析构函数是在对象生命周期接收之前被调用的。

所以我们认为,构造函数或析构函数的功能是不应该包括分配空间和释放空间,两个函数只是在对象分配空间之后、以及释放空间之前系统默认调用的函数。而C里面初始化函数和销毁函数主要就是扮演着这个角色,所以这样看两者是对应的。这里的销毁动作,也并不是释放互斥锁的空间,互斥锁的空间(声明周期)是由系统控制的,如果是全局变量,那么它全局存在,不会因为调用销毁函数而空间被释放了。不过可以这么理解,调用了销毁函数,该互斥锁会被清零,开始初始化以及后来的赋值的结果都被清零。

注意:这一点又在一次的证明了语言是没有能力大小之分的。

4)申请互斥锁

Externint pthread_mutex_lock (pthread_mutex * __mutex);

上面的阻塞式申请,意思是申请不到,当前线程就要阻塞,如下是一个非阻塞申请函数:

Extern int pthread_mutex_trylock(pthread_mutex* __mutex);

非阻塞申请的意思是,申请一下,能申请就申请,不能申请我就走,不管了,干别的去。

5)释放互斥锁

Externint pthread_mutex_unlock (pthread_mutex_t *_mutext);

条件变量

介绍了互斥锁,感觉所有的需要在线程间进行排它访问控制的事情,互斥锁都能够做到,但是互斥锁仍然有些地方是无法做到的。

书上讲了一个例子非常贴切,不过我们尽量寻找一个比较一般的场景。

线程AB,需要排他访问共享数据iA会不断的改变i的值,而B需要等到i为某个值的时候,才能触发一定的动作。看起来似乎没有问题。但是问题出现在,有可能i已经达到B需要的那个值,但是,当时AB争抢i的访问权限的时候,A争到了,并且对i进行了修改,于是后面B就算是再争抢到了,也无法在执行指定的动作了。

有些人说,我可以建立两个线程,做严格控制,让每次A改变了之后,释放该共享数据,一定要留一段时间,能够让B得到控制权。对似乎这个方法合适,但是当线程的复杂都增多,并发的线程增多时,你就会发现这种控制非常困难。

并且,即使通过比较好的时间控制,达到上面的效果,你会发现B执行它的动作只有一次机会(等到i为合适的值),但是就因为这个仅有的机会,B需要不断的读取释放该数据,这样明显浪费了很多的宝贵资源。对于上面的这个场景,我们就希望,B不必要每次都去申请释放该共享数据,我们希望,当i达到指定值的时候能够,主动提醒B线程。

这就是条件变量。

这个“主动通知”的好处在哪里?关键是,B线程如果想要知道i是否达到指定值,就必须每次它被修改的时候,都要申请锁,然后查看它,这个非常浪费的动作,A线程中如果在i到达了某个值的时候,能够主动通知B线程是多么好的过程。这是条件变量的最大好处。

条件变量的使用场景:

多线程互斥访问某个共享数据,但是,线程即使得到了该共享数据的访问权,也需要先判断是否达到访问的条件,才进行相应的操作。所以条件变量是需要和互斥变量同时使用的,是在简单排他访问的基础上,添加了条件的因素。

(1) 定义、初始化、销毁条件变量

Pthread_cond_tcondtion;

Externintpthread_cond_init(pthread_cond_t*__restrict__cond, __constpthread_condattr_t* __restrict__cond_attr);

Extern int pthread_cond_destroy (pthread_cond_t* __cond);

(2) 通知条件发生

Externintpthread_cond_signal (pthread_cond_t *__cond);

Externintpthread_cond_broadcast(pthread_cond_t * __cond);

两者的区别在于,signal只通知第一个等待该条件线程

(3) 等待条件方法

Extern int pthread_cond_wait (pthread_cond_t * __restrict__cond, pthread_mutex_t*__restrict__mutex);
extern int pthread_cond_timedwait (pthread_cond_t * __restrict_cond, pthread_mutex_t*__restrict__mutex,__conststructtimespec* __restrict__abstime);

两者的区别在于,后者会在一定时间范围内等待条件的发生。

并且,从等待条件方法的参数可以看出,条件变量在语法上已经是和互斥锁紧密联系着的。等待条件发生的那个线程,先申请互斥锁,然后再使用其中一个函数申请条件,如果条件不符合,则阻塞,并且默认释放该互斥锁。等到从阻塞返回时,先申请到互斥锁。

其实这里有一个很奇怪的事情,那就是为什么在使用等待条件的之前还要申请互斥锁,既然等待条件方法对互斥锁,有控制,那么大可以直接一句话完成。

与互斥锁配合使用

1) 首先,定义、初始化、以及销毁与互斥锁是一样的。

2) A线程,抓住一次互斥锁后,先对共享变量进行操作,然后判断是否出现特定条件。如果出现了,则首先释放互斥锁,然后通知条件发生。这里要注意,一定要先释放锁,因为,B线程一般就阻塞在等到条件的那个地方,这个地方要唤醒,首先需要申请关联互斥锁。如果没有出现,则照常释放。

3) B线程中,首先申请互斥锁,然后,等待条件。

读写锁

最后我们来看看读写锁,读写锁是对互斥锁基本思想的一个扩充,这种扩充是面向计算机的一个特定策略哲学(可能不只是计算机)。就是一个共享数据,可以同时供多个执行体读访问,但只能同时供一个执行体写访问。这是就是我们通常所说的读写锁。读写锁与互斥锁都称为锁,大家的基本思想都是一样的。条件变量基本上,可以称为互斥锁上的一次扩展而已。

1) 定义、初始化、销毁读写锁

Pthread_rwlock_trwl;

Extern int pthread_rwlock_init (pthread_rwlock_t*__restrict__rwlock, __constpthread_rwlockattr_t * __restrict_attr);

Extern int pthread_rwlock_destroy (pthread_rwlock_t*__rwlock);

2) 申请读锁

Extern intpthread_rwlock_rdlock( pthread_rwlock_t * __rwlock);

Extern int pthread_rwlock_tryrdlock(pthread_rwlock_t * __rwlock);

3) 申请写锁

Extern int pthread_rwlock_wrlock(pthread_rwlock_t * __rwlock);

Extern int pthread_rwlock_trywrlock(pthread_rwlock_t * __rwlock);

4) 解锁

Extern int pthread_rwlock_unlock(pthread_rwlock_t *__rwlock);

=================================================================================================================
一、互斥锁

尽管在Posix Thread中同样可以使用IPC的信号量机制来实现互斥锁mutex功能,但显然semphore的功能过于强大了,在Posix Thread中定义了另外一套专门用于线程同步的mutex函数。

1. 创建和销毁

   有两种方法创建互斥锁,静态方式和动态方式。

   POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。

   动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。

   pthread_mutex_destroy()用于注销一个互斥锁,API定义如下: int pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

2. 互斥锁属性

   互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:

   PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
   PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。


3. 锁操作

   锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。

int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)

pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。

4. 其他

   POSIX线程锁机制的Linux实现都不是取消点,因此,延迟取消类型的线程不会因收到取消信号而离开加锁等待。值得注意的是,如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出回调函数中解锁。

   这个锁机制同时也不是异步信号安全的,也就是说,不应该在信号处理过程中使用互斥锁,否则容易造成死锁。


 

二、条件变量

   条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

1. 创建和注销

条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER

动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。

   注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)

2. 等待和激发

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)

 

   等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

   无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

   激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

3. 其他

pthread_cond_wait()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开pthread_cond_wait(),然后执行取消动作。也就是说如果pthread_cond_wait()被取消,mutex是保持锁定状态的,因而需要定义退出回调函数来为其解锁。

以下示例集中演示了互斥锁和条件变量的结合使用,以及取消对于条件等待动作的影响。在例子中,有两个线程被启动,并等待同一个条件变量,如果不使用退出回调函数(见范例中的注释部分),则tid2将在pthread_mutex_lock()处永久等待。如果使用回调函数,则tid2的条件等待及主线程的条件激发都能正常工作。

#include
#include
#include
pthread_mutex_t mutex;
pthread_cond_t  cond;
void * child1(void *arg)
{
        pthread_cleanup_push(pthread_mutex_unlock,&mutex);  /* comment 1 */
        while(1){
                printf("thread 1 get running \n");
        printf("thread 1 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
        pthread_cond_wait(&cond,&mutex);
                    printf("thread 1 condition applied\n");
        pthread_mutex_unlock(&mutex);
                    sleep(5);
    }
        pthread_cleanup_pop(0);     /* comment 2 */
}
void *child2(void *arg)
{
        while(1){
                sleep(3);               /* comment 3 */
                printf("thread 2 get running.\n");
        printf("thread 2 pthread_mutex_lock returns %d\n",
pthread_mutex_lock(&mutex));
        pthread_cond_wait(&cond,&mutex);
        printf("thread 2 condition applied\n");
        pthread_mutex_unlock(&mutex);
        sleep(1);
        }
}
int main(void)
{
        int tid1,tid2;
        printf("hello, condition variable test\n");
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);
        pthread_create(&tid1,NULL,child1,NULL);
        pthread_create(&tid2,NULL,child2,NULL);
        do{
        sleep(2);                   /* comment 4 */
                pthread_cancel(tid1);       /* comment 5 */
                sleep(2);                   /* comment 6 */
        pthread_cond_signal(&cond);
    }while(1);  
        sleep(100);
        pthread_exit(0);
}

 

   如果不做注释5的pthread_cancel()动作,即使没有那些sleep()延时操作,child1和child2都能正常工作。注释3和注释4的延迟使得child1有时间完成取消动作,从而使child2能在child1退出之后进入请求锁操作。如果没有注释1和注释2的回调函数定义,系统将挂起在child2请求锁的地方;而如果同时也不做注释3和注释4的延时,child2能在child1完成取消动作以前得到控制,从而顺利执行申请锁的操作,但却可能挂起在pthread_cond_wait()中,因为其中也有申请mutex的操作。child1函数给出的是标准的条件变量的使用方式:回调函数保护,等待条件前锁定,pthread_cond_wait()返回后解锁。

条件变量机制不是异步信号安全的,也就是说,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。


 

三、信号灯

   信号灯与互斥锁和条件变量的主要不同在于"灯"的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于"等待"操作,即资源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状态。当然,这样的操作原语也意味着更多的开销。

   信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯。

1. 创建和注销

   POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。

int sem_init(sem_t *sem, int pshared, unsigned int value)
这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变量表征,用于以下点灯、灭灯操作。

int sem_destroy(sem_t * sem) 
被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯注销函数不做其他动作。

2. 点灯和灭灯

int sem_post(sem_t * sem)

点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。

int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
 

sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。

3. 获取灯值

int sem_getvalue(sem_t * sem, int * sval)

读取sem中的灯计数,存于*sval中,并返回0。

4. 其他

sem_wait()被实现为取消点,而且在支持原子"比较且交换"指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的POSIX异步信号安全的API。


 

四、异步信号

   由于LinuxThreads是在核外使用核内轻量级进程实现的线程,所以基于内核的异步信号操作对于线程也是有效的。但同时,由于异步信号总是实际发往某个进程,所以无法实现POSIX标准所要求的"信号到达某个进程,然后再由该进程将信号分发到所有没有阻塞该信号的线程中"原语,而是只能影响到其中一个线程。

   POSIX异步信号同时也是一个标准C库提供的功能,主要包括信号集管理(sigemptyset()、sigfillset()、sigaddset()、sigdelset()、sigismember()等)、信号处理函数安装(sigaction())、信号阻塞控制(sigprocmask())、被阻塞信号查询(sigpending())、信号等待(sigsuspend())等,它们与发送信号的kill()等函数配合就能实现进程间异步信号功能。LinuxThreads围绕线程封装了sigaction()何raise(),本节集中讨论LinuxThreads中扩展的异步信号函数,包括pthread_sigmask()、pthread_kill()和sigwait()三个函数。毫无疑问,所有POSIX异步信号函数对于线程都是可用的。

int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)
设置线程的信号屏蔽码,语义与sigprocmask()相同,但对不允许屏蔽的Cancel信号和不允许响应的Restart信号进行了保护。被屏蔽的信号保存在信号队列中,可由sigpending()函数取出。

int pthread_kill(pthread_t thread, int signo)
向thread号线程发送signo信号。实现中在通过thread线程号定位到对应进程号以后使用kill()系统调用完成发送。

int sigwait(const sigset_t *set, int *sig)
挂起线程,等待set中指定的信号之一到达,并将到达的信号存入*sig中。POSIX标准建议在调用sigwait()等待信号以前,进程中所有线程都应屏蔽该信号,以保证仅有sigwait()的调用者获得该信号,因此,对于需要等待同步的异步信号,总是应该在创建任何线程以前调用pthread_sigmask()屏蔽该信号的处理。而且,调用sigwait()期间,原来附接在该信号上的信号处理函数不会被调用。

如果在等待期间接收到Cancel信号,则立即退出等待,也就是说sigwait()被实现为取消点。


 

五、其他同步方式

   除了上述讨论的同步方式以外,其他很多进程间通信手段对于LinuxThreads也是可用的,比如基于文件系统的IPC(管道、Unix域Socket等)、消息队列(Sys.V或者Posix的)、System V的信号灯等。只有一点需要注意,LinuxThreads在核内是作为共享存储区、共享文件系统属性、共享信号处理、共享文件描述符的独立进程看待的。

 

条件变量与互斥锁、信号量的区别

1).互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行。一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯。

2).互斥锁要么锁住,要么被解开(二值状态,类型二值信号量)

3).由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。

4).互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯即可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。


=============================================================================================================

FUTEX学习
在编译2.6内核的时候,你会在编译选项中看到[*] Enable futex support这一项,上网查,有的资料会告诉你"不选这个内核不一定能正确的运行使用glibc的程序",那futex是什么?和glibc又有什么关系呢?

1. 什么是Futex
Futex是Fast Userspace muTexes的缩写,由Hubertus Franke, Matthew Kirkwood, Ingo Molnar and Rusty Russell共同设计完成。几位都是linux领域的专家,其中可能Ingo Molnar大家更熟悉一些,毕竟是O(1)调度器和CFS的实现者。

Futex按英文翻译过来就是快速用户空间互斥体。其设计思想其实不难理解,在传统的Unix系统中,System V IPC(inter process communication),如 semaphores, msgqueues, sockets还有文件锁机制(flock())等进程间同步机制都是对一个内核对象操作来完成的,这个内核对象对要同步的进程都是可见的,其提供了共享的状态信息和原子操作。当进程间要同步的时候必须要通过系统调用(如semop())在内核中完成。可是经研究发现,很多同步是无竞争的,即某个进程进入互斥区,到再从某个互斥区出来这段时间,常常是没有进程也要进这个互斥区或者请求同一同步变量的。但是在这种情况下,这个进程也要陷入内核去看看有没有人和它竞争,退出的时侯还要陷入内核去看看有没有进程等待在同一同步变量上。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。为了解决这个问题,Futex就应运而生,Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。Linux从2.5.7开始支持Futex。

2. Futex系统调用
Futex是一种用户态和内核态混合机制,所以需要两个部分合作完成,linux上提供了sys_futex系统调用,对进程竞争情况下的同步处理提供支持。
其原型和系统调用号为
    #include
    #include
    int futex (int *uaddr, int op, int val, const struct timespec *timeout,int *uaddr2, int val3);
    #define __NR_futex              240
        
    虽然参数有点长,其实常用的就是前面三个,后面的timeout大家都能理解,其他的也常被ignore。
    uaddr就是用户态下共享内存的地址,里面存放的是一个对齐的整型计数器。
    op存放着操作类型。定义的有5中,这里我简单的介绍一下两种,剩下的感兴趣的自己去man futex
    FUTEX_WAIT: 原子性的检查uaddr中计数器的值是否为val,如果是则让进程休眠,直到FUTEX_WAKE或者超时(time-out)。也就是把进程挂到uaddr相对应的等待队列上去。
    FUTEX_WAKE: 最多唤醒val个等待在uaddr上进程。
    
    可见FUTEX_WAIT和FUTEX_WAKE只是用来挂起或者唤醒进程,当然这部分工作也只能在内核态下完成。有些人尝试着直接使用futex系统调用来实现进程同步,并寄希望获得futex的性能优势,这是有问题的。应该区分futex同步机制和futex系统调用。futex同步机制还包括用户态下的操作,我们将在下节提到。
        
3. Futex同步机制
所有的futex同步操作都应该从用户空间开始,首先创建一个futex同步变量,也就是位于共享内存的一个整型计数器。
当进程尝试持有锁或者要进入互斥区的时候,对futex执行"down"操作,即原子性的给futex同步变量减1。如果同步变量变为0,则没有竞争发生,进程照常执行。如果同步变量是个负数,则意味着有竞争发生,需要调用futex系统调用的futex_wait操作休眠当前进程。
当进程释放锁或者要离开互斥区的时候,对futex进行"up"操作,即原子性的给futex同步变量加1。如果同步变量由0变成1,则没有竞争发生,进程照常执行。如果加之前同步变量是负数,则意味着有竞争发生,需要调用futex系统调用的futex_wake操作唤醒一个或者多个等待进程。

这里的原子性加减通常是用CAS(Compare and Swap)完成的,与平台相关。CAS的基本形式是:CAS(addr,old,new),当addr中存放的值等于old时,用new对其替换。在x86平台上有专门的一条指令来完成它: cmpxchg。

可见: futex是从用户态开始,由用户态和核心态协调完成的。

4. 进/线程利用futex同步
进程或者线程都可以利用futex来进行同步。
对于线程,情况比较简单,因为线程共享虚拟内存空间,虚拟地址就可以唯一的标识出futex变量,即线程用同样的虚拟地址来访问futex变量。
对于进程,情况相对复杂,因为进程有独立的虚拟内存空间,只有通过mmap()让它们共享一段地址空间来使用futex变量。每个进程用来访问futex的虚拟地址可以是不一样的,只要系统知道所有的这些虚拟地址都映射到同一个物理内存地址,并用物理内存地址来唯一标识futex变量。 
      
小结:
1. Futex变量的特征:1)位于共享的用户空间中 2)是一个32位的整型 3)对它的操作是原子的
2. Futex在程序low-contention的时候能获得比传统同步机制更好的性能。
3. 不要直接使用Futex系统调用。
4. Futex同步机制可以用于进程间同步,也可以用于线程间同步。

那么应该如何使用Futex,它和glibc又有什么关系呢?下次继续。

在linux中进行多线程开发,同步是不可回避的一个问题。在POSIX标准中定义了三种线程同步机制: Mutexes(互斥量), Condition Variables(条件变量)和POSIX Semaphores(信号量)。NPTL基本上实现了POSIX,而glibc又使用NPTL作为自己的线程库。因此glibc中包含了这三种同步机制的实现(当然还包括其他的同步机制,如APUE里提到的读写锁)。

Glibc中常用的线程同步方式举例:

Semaphore:
变量定义:    sem_t sem;
初始化:      sem_init(&sem,0,1);
进入加锁:     sem_wait(&sem);
退出解锁:     sem_post(&sem);

Mutex:
变量定义:    pthread_mutex_t mut;
初始化:      pthread_mutex_init(&mut,NULL);
进入加锁:     pthread_mutex_lock(&mut);
退出解锁:     pthread_mutex_unlock(&mut);


这些用于同步的函数和futex有什么关系?下面让我们来看一看:
以Semaphores为例,
进入互斥区的时候,会执行sem_wait(sem_t *sem),sem_wait的实现如下:
int sem_wait (sem_t *sem)
{
  int *futex = (int *) sem;
  if (atomic_decrement_if_positive (futex) > 0)
    return 0;
  int   err = lll_futex_wait (futex, 0);
    return -1;
)
atomic_decrement_if_positive()的语义就是如果传入参数是正数就将其原子性的减一并立即返回。如果信号量为正,在Semaphores的语义中意味着没有竞争发生,如果没有竞争,就给信号量减一后直接返回了。

如果传入参数不是正数,即意味着有竞争,调用lll_futex_wait(futex,0),lll_futex_wait是个宏,展开后为:
#define lll_futex_wait(futex, val) /
  ({                                          /
    ...
    __asm __volatile (LLL_EBX_LOAD                          /
              LLL_ENTER_KERNEL                          /
              LLL_EBX_LOAD                          /
              : "=a" (__status)                          /
              : "0" (SYS_futex), LLL_EBX_REG (futex), "S" (0),          /
            "c" (FUTEX_WAIT), "d" (_val),                  /
            "i" (offsetof (tcbhead_t, sysinfo))              /
              : "memory");                          /
    ...                                      /
  })
可以看到当发生竞争的时候,sem_wait会调用SYS_futex系统调用,并在val=0的时候执行FUTEX_WAIT,让当前线程休眠。

从这个例子我们可以看出,在Semaphores的实现过程中使用了futex,不仅仅是说其使用了futex系统调用(再重申一遍只使用futex系统调用是不够的),而是整个建立在futex机制上,包括用户态下的操作和核心态下的操作。其实对于其他glibc的同步机制来说也是一样,都采纳了futex作为其基础。所以才会在futex的manual中说:对于大多数程序员不需要直接使用futexes,取而代之的是依靠建立在futex之上的系统库,如NPTL线程库(most programmers will in fact not be using futexes directly but instead rely on system libraries built on them, such as the NPTL pthreads implementation)。所以才会有如果在编译内核的时候不 Enable futex support,就"不一定能正确的运行使用Glibc的程序"。

小结:
1. Glibc中的所提供的线程同步方式,如大家所熟知的Mutex,Semaphore等,大多都构造于futex之上了,除了特殊情况,大家没必要再去实现自己的futex同步原语。
2. 大家要做的事情,似乎就是按futex的manual中所说得那样: 正确的使用Glibc所提供的同步方式,并在使用它们的过程中,意识到它们是利用futex机制和linux配合完成同步操作就可以了。
    
如果只是阅读理解,到这里也就够了,不过我们需要用实际行动印证一下,我们的理解是否正确。在实际使用过程中还遇到什么样的问题? 下次继续。

上回说到Glibc中(NPTL)的线程同步方式如Mutex,Semaphore等都使用了futex作为其基础。那么实际使用是什么样子,又会碰到什么问题呢?
先来看一个使用semaphore同步的例子。

sem_t sem_a;
void *task1();

int main(void){
 int ret=0;
 pthread_t thrd1;
 sem_init(&sem_a,0,1);
 ret=pthread_create(&thrd1,NULL,task1,NULL); //创建子线程
 pthread_join(thrd1,NULL); //等待子线程结束
}

void *task1()
{
  int sval = 0;
  sem_wait(&sem_a); //持有信号量
  sleep(5); //do_nothing
  sem_getvalue(&sem_a,&sval);
  printf("sem value = %d/n",sval);
  sem_post(&sem_a); //释放信号量
}

程序很简单,我们在主线程(执行main的线程)中创建了一个线程,并用join等待其结束。在子线程中,先持有信号量,然后休息一会儿,再释放信号量,结束。
因为这段代码中只有一个线程使用信号量,也就是没有线程间竞争发生,按照futex的理论,因为没有竞争,所以所有的锁操作都将在用户态中完成,而不会执行系统调用而陷入内核。我们用strace来跟踪一下这段程序的执行过程中所发生的系统调用:
...
20533 futex(0xb7db1be8, FUTEX_WAIT, 20534, NULL
20534 futex(0x8049870, FUTEX_WAKE, 1)   = 0
20533 <... futex resumed> )             = 0
... 
20533是main线程的id,20534是其子线程的id。出乎我们意料之外的是这段程序还是发生了两次futex系统调用,我们来分析一下这分别是什么原因造成的。

1. 出人意料的"sem_post()"
20534 futex(0x8049870, FUTEX_WAKE, 1)   = 0
子线程还是执行了FUTEX_WAKE的系统调用,就是在sem_post(&sem_a);的时候,请求内核唤醒一个等待在sem_a上的线程,其返回值是0,表示现在并没有线程等待在sem_a(这是当然的,因为就这么一个线程在使用sem_a),这次futex系统调用白做了。这似乎和futex的理论有些出入,我们再来看一下sem_post的实现。
int sem_post (sem_t *sem)
{
  int *futex = (int *) sem;
  int nr = atomic_increment_val (futex);
  int err = lll_futex_wake (futex, nr);
  return 0;
}
我们看到,Glibc在实现sem_post的时候给futex原子性的加上1后,不管futex的值是什么,都执行了lll_futex_wake(),即futex(FUTEX_WAKE)系统调用。
在第二部分中(见前文),我们分析了sem_wait的实现,当没有竞争的时候是不会有futex调用的,现在看来真的是这样,但是在sem_post的时候,无论有无竞争,都会调用sys_futex(),为什么会这样呢?我觉得应该结合semaphore的语义来理解。在semaphore的语义中,sem_wait()的意思是:"挂起当前进程,直到semaphore的值为非0,它会原子性的减少semaphore计数值。" 我们可以看到,semaphore中是通过0或者非0来判断阻塞或者非阻塞线程。即无论有多少线程在竞争这把锁,只要使用了semaphore,semaphore的值都会是0。这样,当线程推出互斥区,执行sem_post(),释放semaphore的时候,将其值由0改1,并不知道是否有线程阻塞在这个semaphore上,所以只好不管怎么样都执行futex(uaddr, FUTEX_WAKE, 1)尝试着唤醒一个进程。而相反的,当sem_wait(),如果semaphore由1变0,则意味着没有竞争发生,所以不必去执行futex系统调用。我们假设一下,如果抛开这个语义,如果允许semaphore值为负,则也可以在sem_post()的时候,实现futex机制。

2. 半路杀出的"pthread_join()"
那另一个futex系统调用是怎么造成的呢? 是因为pthread_join();
在Glibc中,pthread_join也是用futex系统调用实现的。程序中的pthread_join(thrd1,NULL); 就对应着 
20533 futex(0xb7db1be8, FUTEX_WAIT, 20534, NULL
很好解释,主线程要等待子线程(id号20534上)结束的时候,调用futex(FUTEX_WAIT),并把var参数设置为要等待的子线程号(20534),然后等待在一个地址为0xb7db1be8的futex变量上。当子线程结束后,系统会负责把主线程唤醒。于是主线程就
20533 <... futex resumed> )             = 0
恢复运行了。
要注意的是,如果在执行pthread_join()的时候,要join的线程已经结束了,就不会再调用futex()阻塞当前进程了。

3. 更多的竞争。
我们把上面的程序稍微改改: 
在main函数中:
int main(void){
 ...
 sem_init(&sem_a,0,1);
 ret=pthread_create(&thrd1,NULL,task1,NULL);
 ret=pthread_create(&thrd2,NULL,task1,NULL);
 ret=pthread_create(&thrd3,NULL,task1,NULL);
 ret=pthread_create(&thrd4,NULL,task1,NULL);
 pthread_join(thrd1,NULL);
 pthread_join(thrd2,NULL);
 pthread_join(thrd3,NULL);
 pthread_join(thrd4,NULL);
 ...
}

这样就有更的线程参与sem_a的争夺了。我们来分析一下,这样的程序会发生多少次futex系统调用。
1) sem_wait()
    第一个进入的线程不会调用futex,而其他的线程因为要阻塞而调用,因此sem_wait会造成3次futex(FUTEX_WAIT)调用。
2) sem_post()
    所有线程都会在sem_post的时候调用futex, 因此会造成4次futex(FUTEX_WAKE)调用。
3) pthread_join()
    别忘了还有pthread_join(),我们是按thread1, thread2, thread3, thread4这样来join的,但是线程的调度存在着随机性。如果thread1最后被调度,则只有thread1这一次futex调用,所以pthread_join()造成的futex调用在1-4次之间。(虽然不是必然的,但是4次更常见一些)    
所以这段程序至多会造成3+4+4=11次futex系统调用,用strace跟踪,验证了我们的想法。
19710 futex(0xb7df1be8, FUTEX_WAIT, 19711, NULL
19712 futex(0x8049910, FUTEX_WAIT, 0, NULL
19713 futex(0x8049910, FUTEX_WAIT, 0, NULL
19714 futex(0x8049910, FUTEX_WAIT, 0, NULL
19711 futex(0x8049910, FUTEX_WAKE, 1
19710 futex(0xb75f0be8, FUTEX_WAIT, 19712, NULL
19712 futex(0x8049910, FUTEX_WAKE, 1
19710 futex(0xb6defbe8, FUTEX_WAIT, 19713, NULL
19713 futex(0x8049910, FUTEX_WAKE, 1
19710 futex(0xb65eebe8, FUTEX_WAIT, 19714, NULL
19714 futex(0x8049910, FUTEX_WAKE, 1)   = 0
(19710是主线程,19711,19712,19713,19714是4个子线程)

4. 更多的问题
事情到这里就结束了吗? 如果我们把semaphore换成Mutex试试。你会发现当自始自终没有竞争的时候,mutex会完全符合futex机制,不管是lock还是unlock都不会调用futex系统调用。有竞争的时候,第一次pthread_mutex_lock的时候不会调用futex调用,看起来还正常。但是最后一次pthread_mutex_unlock的时候,虽然已经没有线程在等待mutex了,可还是会调用futex(FUTEX_WAKE)。这又是什么原因造成的呢?留给感兴趣的同学去分析吧。

小结:
1. 虽然semaphore,mutex等同步方式构建在futex同步机制之上。然而受其语义等的限制,并没有完全按futex最初的设计实现。
2. pthread_join()等函数也是调用futex来实现的。
3. 不同的同步方式都有其不同的语义,不同的性能特征,适合于不同的场景。我们在使用过程中要知道他们的共性,也得了解它们之间的差异。这样才能更好的理解多线程场景,写出更高质量的多线程程序。

至此futex的学习就告一段落了,希望对大家有所帮助。



阅读(2180) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~