linux 线程管理、同步机制等

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

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

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

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

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

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

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

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

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

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

线程基本管理创建线程

/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中的某个部分做该做的事。主刀者做它的事,输血者做输血的事,观察者看仪器。他们各司其职、但也并是不保持绝对的独立。

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

线程退出

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

/usr/include/bits/pthreadtypes.h

Extern void pthread_exit (void * _retval);

线程退出的情景有:

1)调用pthread_exit

2)调用pthread_cancel

3)所属进程退出

4)线程函数结束

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

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

等待线程

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

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是当前线程的线程id,pointer是某个地址为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);

条件变量

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

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

线程A、B,需要排他访问共享数据i,A会不断的改变i的值,而B需要等到i为某个值的时候,才能触发一定的动作。看起来似乎没有问题。但是问题出现在,有可能i已经达到B需要的那个值,但是,当时A、B争抢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);

只有这样才不会被“不可能”束缚,才能不断超越自我。

linux 线程管理、同步机制等

相关文章:

你感兴趣的文章:

标签云: