Linux编程---线程

首先说一下线程的概念.事实上就是运行在进程的上下文环境中的一个运行流.普通进程仅仅有一条运行流,可是线程提供了多种运行的路径并行的局面.

同一时候,线程还分为核心级线程和用户级线程.主要差别在属于核内还是核外.

核心级线程,地位基本和进程相当,由内核调度.也就是说这样的系统时间片是按线程来分配的.这样的线程的优点就是能够适当的运用SMP,即针对多核CPU进行调度.

用户级线程,在用户态来调度.所以相对来说,切换的调度时间相对核心级线程来说要快不少.可是不能针对SMP进行调度.

对于如今的系统来说,纯粹的用户级线程仅仅存在于实验室中吧.

对于Linux中实现的线程来说,支持LWP(轻量级进程).把用户级线程映射到内核级上.简单来说,Linux仅仅有内核级进程,不存在真正的线程.可是我们也能够把LWP叫做线程.

线程和进程对操作系统来说相当于是平行的关系,但大部分资源是隶属于进程的.假设进程挂了,所有线程也会终止.(直接赋值其进程的页表,然后改动特殊的部分就可以创建一下线程~复习一下虚拟存储~)

顺带一提erlang虚拟机下的轻量级线程是虚拟机下实现的。应该叫进程。.我记得在哪看过,说erlang切换线程比c运行的线程切换还快.应该能够分类成用户级线程吧。

而Linux仅仅有LWP..

这里我还有个疑问,就是Linux的线程是否是环保线程.这个概念也是今天刚看<现代操作系统>中提到的.意思就是对于进程来说,线程并不马上释放资源,而是等到进程结束再释放.这样就省去了线程又一次生成的开销.对于server来说应该是非常实用的一个策略呢.有知道的吗?

--------------------补充说明切割线-----------------------

又看了些书.发现实际上linux的线程创建过程主要调用的是clone函数.

这个函数的第二个參数有好几种状态选择.这些选择决定了clone出来的进程是一般所说的线程还是一个进程.

而且有下面几种标志能够选择:

CLONE_VM 置1:创建线程--共享地址空间   置0:创建进程,不共享地址空间,但会复制

CLONE_FS 共享umask 不共享

CLONE_FILES 共享文件描写叙述符 拷贝文件描写叙述符

CLONE_SIGHAND 共享信号句柄表 赋值信号句柄表

CLONE_PID 新线程获得旧的PID 新线程获得自己的PID

CLONE_PARENT 新线程与调度这有同样的父亲 新线程的父亲是调用者

Linux对进程标识符PID和任务标识符TID进行了区分!!而且两个都在task_struct结构中.

当用clone函数创建一个新进程而不须要和旧进程共享不论什么信息时,PID被设置成一个新值(新进程?fork?

).否则任务得到一个新的任务标识符,可是PID不变.

TID也就是我后面会说的线程标识符.

预计pthread库中,应该就是把这些标志都选上,然后创建的.

----------------------------------------------------------------

使用线程的程序一般具有一下特征:

1.可以与其它任务并行运行.

2.有可能会被堵塞较长时间,但这时候其它进程可并发运行.

3.须要回应异步事件.毕竟异步本身就是不确定堵塞时间的.

4.线程使用CPU的时间足够长.不然切换的代价也不少.

这里要写的是Pthread线程.也就是POSIX定义的线程接口.这个接口包括100多个函数,全部函数名都已pthread_开头.功能上大致分为三类:

线程管理: 这类函数负责线程的创建,终止,汇合,取消,以及线程属性的设置和查询等.

线程同步: Pthread提供了相互排斥变量,条件变量,栅栏(屏障)变量等手段支持线程间的同步.

操作线程专有数据: 多线程程序中,全局数据分为全部线程都能够訪问的共享数据和单个线程内全部函数都能够訪问的线程专有数据.

这里要注意一点:差点儿全部函数都部通过errno来报错.运行成功均返回0.除开pthread_getspecific全然不报错之外,其余的返回错误号.

可是对于单独的一个线程,报错的时候改动你的errno,其它线程是无法干扰的.实际上Linux的errno是一个局部变量(这里也是网上查的,只是都是非常老的帖子里面的,有错误请指正)

线程标识:

pthread_t pthread_self(void);   用于获得线程ID,即TID

pthread_equal(pthread_t t1,pthread_t t2);   用于比較两个线程标号,pthread_t可能并非整形.

一.线程管理

1.创建线程

int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void*),void * restrict arg);

第一个參数传回其线程的TID,第二个參数用来设置线程的属性,一般写NULL,第三个为线程開始运行的函数指针,第四个參数为传递给线程的唯一一个參数.

注意Pthread的返回值是统一的.之后都不会再说了.

2.终止线程

int pthread_exit(void *value_ptr);

这个函数是用于线程自己终止自己的.当中的參数相当于是返回值,当还有一个线程调用pthread_join()等待其结束时,这个值会给pthread_join中的參数.

这里须要注意的就是,不要让value_ptr指向局部变量,由于线程结束,其资源会被回收.最好养成用malloc直接申请一个的习惯.免得在线程分离的情况下出现故障.(假设Linux是环保线程的话,那么这个就不用操心太多了.)

同一时候,线程的回收并不回收不论什么进程相关的资源.

假设你用exit()的话,那么整个进程都会被回收..而不是回收线程.而且注意,假设main线程结束了的话,那么线程也不运行了!!!假设你想让线程运行完再结束进程,一定要设置好同步!!pthread_join就非常好.

3.等待线程终止

int pthread_join(pthread_t thread,void **value_ptr);

第一个參数是指定等待线程的TID.第二个则是调用pthread_exit返回的參数.

而且这个函数是会堵塞的!这个函数类似进程中的wait函数,还具有释放资源的功能.这还涉及到一个可汇合与分离的线程概念.

可汇合的意思就是说,线程的资源在返回给pthread_join之后才释放.

分离的意思就是资源的释放有系统来搞定.

这样做的目的是尽可能的节省系统资源,提高效率.可能一般小程序体现不出什么,可是对于server程序来说,一点性能的提升就能干非常多事~

这里的join仅仅能用于可汇合的线程,分离线程则不行.

默认的线程创建出来的都是可汇合的.

另一点,线程是能够互相等待的.并非仅仅有主线程才干等待其它线程.不论什么线程都能够等待另外一个线程的结束.

4.分离线程

int pthread_detach(pthread_t thread);

指定一个TID号,然后这个线程就分离了.so easy...

当然,你得细致考虑这个线程是否须要返回才是.

假设调用了两次的话,那么Linux下会返回EINVAL.

分离之后线程就不由父线程管了.pthread_join就不会等待它了.

注意,分离线程的TID在线程终止后能够马上又一次分配给其它创建的线程.

当应用程序须要与分离的线程同步,应当现判别该线程是否已经终止.能够用全局变量来当作标志.避免分离的进程随主线程的exit而终止.

5.创建特殊的线程

这个是补充之前的创建线程的.

这里用到了非常多函数...我就不明确为什么不直接用结构体赋值取代...非要搞函数..

1)线程属性对象的初始化和销毁函数

int pthread_attr_init(pthread_attr_t *attr)

int pthread_attr_destroy(pthread_attr_t *attr)

初始化包括两件事:分配空间,赋初始值.所以非常明显destroy是用来释放空间的.

注意这里传值传的是一个指针,而不是结构体!

2)线程分离状态的查询与设置函数

int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);

int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate);

get返回之后,设置第二个參数为PTHREAD_CREATE_DETACHED或者PTHREAD_CREATE_JOINABLE.意思非常明显了.

set则能够通过第二个參数设置.选项就是上面两个.

3)线程栈的查询和设置函数

int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,size_t *restrict stacksize);

int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);

第二个參数同上,一个取值,一个设置.而且设置的时候值不能小于PTHREAD_STACK_MIN

这里书上没有说,可是我查的是按字节来分配的.通常Linux的栈大小为10M.

int pthread_attr_getstackaddr(const pthread_attr_t *restrict attr,void **restrict stackaddr);

int pthread_attr_setstackaddr(const pthread_attr_t *restrict attr,void *stackaddr);

这个就是获得和设置栈的虚拟地址没多少可说的...

这里要注意的就是:

--对于多个线程,不要用统一的属性来设置地址.毕竟虚拟地址空间是统一的.

--这个线程应当是可汇合的.(这里我不太明确为什么应该这样,详细看源代码再说吧)

--这个线程栈的保护区应当是由自己设置的.

--内存对齐.

int pthread_attr_getstack(const pthread_attr_t *restrict attr,void **restrict stackaddr,size_t *restrict stacksize);

int pthread_attr_setstack(const pthread_attr_t *restrict attr,void *restrict stackaddr,size_t restrict stacksize);

因为分别获取或设置效率较低,所以干脆一次性来获取或设置..

4)栈一处保护区大小的查询和设置函数

int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict guardsize);

int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);

栈保护区是在线程栈之后的一片区域,而且设置了特殊处理假设产生栈溢出的话,那么线程就会收到一个SIGSEGV信号.

通常有两种情况须要调整其大小:

--节省存储空间.对于嵌入式设备比較实用吧.

--线程须要更大的空间存放局部变量时.比方用到了深度递归程序的话~(预计线程的进入函数也能递归~)

6.取消线程

与之前的线程用exit终止自己不一样,这个是一个线程来取消另外一个线程.对于一些并行编程应该非常有作用吧.比方你用BFS搜索一个解空间,每一个线程分配一个搜索范围.搜索到答案之后,其余在搜索解的线程就能够取消掉了.

同一时候,这个取消是”友好的”,意思就是并非强行让线程终止.就和IO差点儿相同.有一个缓存的过程.自己能够去查看是否缓存好,也能够通过异步来实现.

可取消的属性

--可取消状态:这个表示能否够被其它线程取消.默认属性是同意的.

--可取消类型:默认是延迟取消.也就是线程自己检查是否可被取消.这就类似同步IO,必须自己去检查是否有数据来.为了效率还有第二种异步类型.

假设要改变默认值,能够用以下两个函数:

int pthread_setcancelstate (int state,int *oldstate);

int pthread_setcanceltype (int tyep,int *oldtype);

第一个參数都是新的设置,第二个则是原来的设置值.

state能够取值为PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE

type能够取值为PTHREAD_CANCEL_DEFERRED和PTHREAD_CANCEL_ASYNCHRONOUS.

注意,假设对一个不同意取消的线程发送一个请求,那么请求会保持.这一点和信号不同.

明白了属性,以下再来介绍实际操作.

int pthread_cancel(pthread_t target_thread);

这个是发送取消请求.參数是线程ID.而且不等待目标线程的终止.

延迟类型下,分散在非常多地方..比方各种系统调用和phread函数中都会检查的.这些个函数太多了,我就不细写了....详细能够查手冊.只是能够知道的是,这些函数中多数为慢系统调用(有可能被无限堵塞).

延迟类型还能够用专门的函数来检查:

void pthread_testcancel();

检查到了之后直接取消线程.

异步类型就是随便什么时候终止都行了.这就必需要需要保证相互排斥量,线程专有数据,堆空间什么的没在使用了.

当然,为了异步编程方便,实际上在每一个线程中有一个隐含的清理器栈,就是个函数指针栈.每次取消线程或者退出线程的时候会自己主动运行这些栈中的函数.在必要时,也能够自己弹出栈,然后运行.

void pthread_cleanup_push(vodi (*routine )(void *),void *arg);

void pthread_cleanup_pop(int execute);

第一个函数是入栈.其第二个參数是在第一个參数运行时,送给第一个參数的的參数(好绕)....

第二个函数则是从其栈中弹出.当參数不是0的时候,会运行!

感觉这两个函数还蛮方便的.比析构好用多了.

7.线程调度

1)线程调度范围

POSIX定义了两种调度模式:进程调度模式和系统调度模式.前者的竞争范围为进程内,后者则为进程间.感觉就是所谓的用户级线程和内核级线程.但Linux仅仅有系统调度模式.在APUE也没有相关的内容.以后做了实验再来补一篇博客吧.

PTHREAD_SCOPE_PROCESS表示进程调度模式.

PTHREAD_SCOPE_SYSTEM 表示系统调度模式.

这么分,主要是为了区分线程竞争的对手.前者仅仅在进程内区分.后者则和其它进程一起竞争.

主要在phread_attr_t类型变量中设置后,传递给pthread_create.

2)线程调度策略与优先级

SCHED_FIFO 基于优先级的先进先出调度策略.不同优先级不同队列.这样的策略可能让高优先级线程独占系统.

SCHED_RR 循环调度.也就是时间片轮转.也有FIFO的优先级队列.尽管是时间片轮转,但仍有可能独占系统.

SCHED_OTHER 这样的一般类UNIX系统默觉得对进城的系统分时调度策略.这随系统不同,策略也不同,所以不可移植.当然,也能够採用自定义的策略.

int sched_get_priority_max(int policy);

int sched_get_priority_min(int policy);

两个函数能够用来得到优先级的最大值和最小值.都是个int类型.

參数的意思就是上面所说的策略.把宏定义填进去就能够了.

POSIX没有对SCHED_OTHER定义优先级范围,但自己定义的范围一定要在min到max的返回值之间.

3)线程调度属性

--竞争范围属性

PTHREAD_SCOPE_PROCESS和PTHREAD_SCOPE_SYSTEM

个人感觉Linux中应该没有PROCESS调度才对...由于Linux的线程和进程都是用的一种数据结构并且Linux实现的是轻量级线程.....这样应该就仅仅有一种调度方式了吧....我在网上问了也没人回答....

--继承属性

这个主要是指明新创建的线程怎样获得他的调度策略和相连的调度參数.

PTHREAD_INHERIT_SCHED和PTHREAD_EXPLICIT_SCHED

前者表示继承,后者则从后面两个属性中设置.

--调度策略属性

SCHED_FIFO

SCHED_RR

SCHED_OTHER

--调度參数属性(包括优先级)

这是一个对程序猿不透明的结构体.详细的能够查看sched.h.可是至少包括一个sched_priority的成员.对于SCHED_FIFO和SCHED_RR来说sched_priority是唯一的调度參数.

有以下这么些函数来获取和设置

int pthread_attr_getscope(const pthread_attr_t *restrict attr,int *restrict contentionscope);

int pthread_attr_setscope(pthread_attr_t *attr,int contentionscope);

int pthread_attr_getinheritsched(pthread_attr_t *attr,int * inherit);

int pthread_attr_setinheritsched(pthread_attr_t *attr,int *inherit);

int pthread_attr_getschedpolicy(pthread_attr_t *attr,int *policy);

int pthread_attr_setschedpolicy(pthread_attr_t *attr,int *policy);

int pthread_attr_getschedparam(const pthread_attr_t *restrict attr,struct sched_param *restrict param);

int pthread_attr_setschedparam(const pthread_attr_t *restrict attr,const struct sched_param*restrict param );

scope范围,inherit继承,policy策略,param參数(优先级)

这些函数成功都返回0.一般多数系统都不同意用户应用随便设置线程的调度属性,仅仅有特权用户才行.而且一定要创建之前设置PTHREAD_EXPLICIT_SCHED属性.

4)动态改变调度策略和优先级

int pthread_getschedparam(pthread_t thread ,int *restrict policy,struct sched_param *restrict param );

int pthread_setschedparam(pthread_t thread ,int policy,const struct sched_param *param );

int pthread_setschedprio(pthread_t thread ,int prio);

这个感觉没什么好说的.前两个是改变策略和參数.第三个直接就是改变优先级.

当策略和优先级改变时,线程从执行状态切换至就绪状态,并放置到新的优先级队列中.非常多系统对于SCHED_FIFO来说,一般不让随便设置成最高优先级.

------------------补充的切割线-----------------

我看<现代操作系统>上面说了有三种调度方式.前两种和上面一致.最后一种叫分时.应该就是SCHED_OTHER吧.感觉翻译有点问题.详细以后看源代码再专门写一篇Linux调度策略的文章吧.

这里的优先级在Linux下是0~139.

当中前0~99是实时优先级.100~139则是非实时优先级.

而且Linux的时钟是1000HZ的.所以最小时间片为1ms.

对于非实时的一般就执行的时间片就比較少了.100级的时间片为800ms,而139级仅仅有5ms.

这里的Linux内核版本号为2.6.

------------------几个月后的切割线-------------------

这里的优先级好像不正确头...Linux支持两种优先级.一种是nice值,范围在(-20~19)..一种是实时优先级,范围在(0~99)

至于上面说的线程优先级可能又有不同..但Linux下线程是LWP....至少这一点存疑吧..以后看kernel源代码再说..

----------------------------------------------------

今天又看了遍<现代操作系统>..优先级在100~139的是非实时线程的优先级..感觉分得好清楚....现代这本数的作者比Linux内核开发的大神都清楚啊...

---------------------------------------------------------------

8.线程与信号

在多线程中,信号对每一个线程都是共享的.对于每一种信号,全部线程所相应的动作都是同样的.并且全部线程可能同一时候运行这个信号.即POSIX不支持线程范围的信号动作.

为了保证信号的一致性,统一建立全部信号的信号动作,最好用一个线程来完毕其信号的设置和改变.一定不要在可能被多线程调用的函数中改变信号的动作,由于你无法保证仅仅有一个线程调用其函数.

仅仅要信号的动作使得线程终止,暂停或者继续,就相同会使得整个进程终止,暂停或者继续.也就是说发送SIGKILL,SIGSTOP,SIGCONT这三种信号,都是针对进程而言的.要终止线程,能够用cancel来取消线程.这样就避免了整个进程由于线程的原因产生不可预測的行为.

多线程中,信号也分为同步和异步.同步信号由线程自己来处理.对于异步就复杂了.

异步信号假设是发送给某个线程的,那么仅仅有这个线程能收到信号.

假设是发给进程的.那么进程中全部线程都有可能收到,可是仅仅有一个未屏蔽该信号的线程来处理.详细由哪个来运行也不确定.假设想要一个线程来接受某个异步信号,那么全部的线程都该屏蔽这个信号.

当一个信号被多个线程共享,那么这个信号句柄就得是可重入的.由于接收到信号可能让多个线程运行这个句柄.对于PISIX指明全部函数一定要是可重入的,可是Pthread则不是全部函数都能够重入.

当多线程共享数据时,不可避免的要用线程同步.假设想要用线程同步函数或者不可重入函数,那么最好不要用sigaction来建立句柄.能够使用sigwait()函数来同步异步信号.

1)信号屏蔽

正如我上面说所,每一个线程都是有自己的屏蔽信号的.

int pthread_sigmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);

这个函数与sigprocmask类似,可是这个函数专门用于检測或改变(或者两个都有)调用线程的私有信号屏蔽.

how參数有下面几种模式:

SIG_BLOCK: 即将set所指信号集的信号加入到当前信号屏蔽中

SIG_UNBLOCK: 将set所指信号集的信号从当前信号屏蔽中去除.

SIG_SETMASK: 将set所指信号集的信号设置为当前信号屏蔽中去.

一个线程的屏蔽信号,会从创建它的线程中继承.

当信号被屏蔽的时候,假设有此信号来则会一直悬挂到被解除屏蔽或者调用了sigwait或者线程结束.

2)向线程发送信号

int pthread_kill(pthread_t thread,int sig);

向指定线程,发送一个sig信号.当sig为0时,和kill()类似,仅仅检查指定的线程是否存在.

3)等待信号

int sigwait(const sigset_t *restrict set,int *restrict sig);

这个函数直接从set信号集中等待信号,而且一直堵塞直到有信号来,然后直接返回,不须要设置句柄.当集合中的多个信号同一时候悬挂时,那么先返回信号数比較低的.

假设一直没来信号集中的信号,那么会无限期的堵塞下去.这时候就能够考虑用sigtimedwait函数了.这样就能够设置一个超时时间了.

注意用的时候还是得把其它线程中的set里的信号屏蔽掉.否则其它线程就有可能接受这个信号.

利用这个函数能够方便的实现让一个特定的线程同步等待发送给进程的异步信号.简单来说就是异步信号处理线程.

一种新的时间通知方法:SIGEV_THREAD

二.线程同步

这一部分主要说的就是一些锁的运用.边看边回顾操作系统~

这里我第一次晓得栅栏变量用来同步.原来学操作系统中都没有讲过.网上资料也好少..按我自己的理解,栅栏同步就是设置一个阈值,然后一到阈值就同步.这本书也没怎么写到栅栏同步,我想应该非常easy学的,以后遇到再写吧.

1.相互排斥变量

1)初始化与销毁

pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

第一行的仅仅能用于静态初始化的变量,即全局的,不能用于malloc的.

init的第二个变量是用来指明其相互排斥变量的属性.一般就用NULL,设置成系统默认值.而且最好仅仅初始化一次,尽管Linux下会正常返回,可是easy产生不easy发现的错误.初始化之后就不能改变其属性了.详细看以下.

2)相互排斥变量的属性

属性例如以下:

--进程共享属性.

PTHREAD_PROCESS_PRIVATE //仅由同一个进程内的线程使用,这是默认值

PTHREAD_PROCESS_SHARED //能够由多个进程的线程使用  注意是多个进程.一般这个效率比較低,尽量避免使用.并且在进程终止前一定要释放,否则可能导致死锁.并且这个相互排斥变量的存储空间须要应用自己来分配.

--类型属性值

PTHREAD_MUTEX_NORMAL //基本类型,无特定功能,最快的一种,错误检查最少

PTHREAD_MUTEX_RECURSIVE //递归类型,能够多次加锁,就是信号量的意思

PTHREAD_MUTEX_ERRORCHECK //检查并报告简单的使用错误.主要用来帮助调试

PTHREAD_MUTEX_DEFAULT //这个是默认类型.Linux会映射为NORMAL类型

pthread_mutexattr_t的初始化什么的也要由专门的函数来取代...

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

这里destroy之后仍然能够再次init.

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);

int pthread_mutexattr_getpshared(pthread_mutexattr_t *attr,int *restrict pshared);

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int type);

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int *restrict type);

上面两个是用于进程共享属性的.以下两个用于类型属性的.

3)相互排斥变量的加锁与解锁.

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

第一个加锁会堵塞,第二个不会.

第二个失败会返回EBUSY.但假设锁的属性是PTHREAD_MUTEX_RECURSIVE的话,那么锁计数器会加一.而且返回0.

锁的操作都好复杂啊...不同属性还有不同的处理方式...以下是补充

PTHREAD_MUTEX_NORMAL //不进行死锁检測.反复加锁会导致死锁.

PTHREAD_MUTEX_ERRORCHECK //反复加锁或对未加锁的mutex进行解锁时,错误返回

PTHREAD_MUTEX_RECURSIVE //信号量的概念,对未锁或者已经解锁的解锁,错误返回

PTHREAD_MUTEX_DEFAULT //Linux默认会置位NORMAL.其它系统,会导致不确定结果.

4)相互排斥变量与spin锁

这个就是自旋锁.针对一些操作时间短的过程加锁.这种话就比那些使用相互排斥锁的要快一些.毕竟没有上下文的切换,降低了系统调用的时间.

int pthread_spin_init(pthread_spinlock_t *lock,int pshared):

int pthread_spin_destroy(pthread_spinlock_t *lock):

int pthread_spin_lock(pthread_spinlock_t *lock):

int pthread_spin_trylock(pthread_spinlock_t *lock):

int pthread_spin_unlock(pthread_spinlock_t *lock):

功能和相互排斥锁全然一样.就是线程不必堵塞直到解锁,而是通过轮询,不断的查询.所以这个锁仅仅能用于一些加锁过程比較短的地方.

2.读写锁

因为相互排斥锁和自旋锁一定时间内仅仅同意一个线程执行,所以非常可能导致程序变成串行的.这样就须要读写锁来改善程序性能了.这个锁主要用于读操作频繁但写操作非常少的共享数据.每次能够由多个线程读,可是仅仅能一个线程写.虽说提高了并行性,可是上锁和解锁的时间比相互排斥量开销大.大部分时候还是尽量选择相互排斥锁.

一般而言,占有锁时间比較短时使用相互排斥锁;时间较长且读操作多,写操作少时才使用读写锁.

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITALIZER

int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock,const pthread_rwlock_t *restrict attr);

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

基本和之前的相互排斥锁一样.我就不多写了.

可是读写锁仅仅有共享属性的设置,没有类型属性的设置.

用pthread_rwlockattr_setpshared和pthread_rwlockattr_getpshared详细的我就不多说了.

读锁

pthread_rwlock_rdlock

pthread_rwlock_tryrdlock

写锁

pthread_rwlock_wrlock

pthread_rwlock_trywrlock

系统一般优先考虑运行写锁.

解锁

pthread_rwlock_unlock

这个写和读是统一的.

写到这里有点疑问,就是为什么读写锁不仅仅把写的部分上锁而读的部分任意?

事实上读的时候也是要避免写的,假设之上写锁,非常可能会读出错误结果.

3.条件变量

简单来说,条件变量是为了把解锁和等待变为原子动作所想到的一个方法.

以生产者-消费者模型来说.当产品为空时,消费者必须堵塞等待生产者生产完毕.这里可分为三个步骤:

1)释放相互排斥变量,让生产者可以生产产品.

2)消费者线程必须堵塞

3)当能够消费时,解除堵塞.

这里有可能会让线程永久堵塞.书上写的我不太明确,我试着去理解下面意思.

首先,这里的消费者线程看到相互排斥量没锁,自己锁上,然后进入运行步骤.再推断是否满足条件,假设满足条件,就进行工作.然后释放锁.假设不满足条件,那么就进入队列,而且堵塞自己.当生产者刚好生产出来一个产品时,唤醒队列中的线程.假设线程做完了条件推断,刚释放完相互排斥变量,正处于进入队列的过程中.那么生产线程可能就不能唤醒这个消费线程(由于还没进入队列,但已经解锁了.解锁的中途生产者线程生产了产品).假设生产者在唤醒的时候队列中没有线程.那么这个就永久堵塞了.

这个东西主要是用来实现管程的..

个人觉得仅仅要有新线程进来,那么就会激活这个线程...或者仅仅要这个检查到没有线程等待,过一段时间再检測就好了...只是安全起见还是用条件变量吧.

创建和销毁条件变量

pthread_cond_t cond = PTHREAD_COND_INITALIZER

int pthread_cond_init (pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

int pthread_cond_destroy(pthread_cond_t *cond);

第一个是静态初始化.第二个是动态初始化.

同之前的init的attr一般为NULL,自己主动分配成系统默认的状态.

当用init初始化时,才用destroy来销毁条件变量.

假设你把一个正在使用的条件变量用destroy的话,会正常返回.

条件变量属性

这里就仅仅有进程共享属性.PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED两个.

一般这个都不用,只是还是写一下.

int pthread_condattr_init(pthread_condattr_t *attr);

int pthread_condattr_destroy(pthread_condattr_t *attr);

int pthread_condattr_setpthared(pthread_condattr_t *attr,int *pshared);

int pthread_condattr_getpshared(pthread_condattr_t *attr,int *pshared);

个人感觉全然能够写一个函数把这个封装起来.反正也就仅仅一种用途.

等待条件变量

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

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

两个函数区别就是第二个函数是有时间限制的.

注意!线程在调用这两个函数之前必须是锁住的.而且在堵塞之前会自己主动释放mutex.返回时,又一次获得该相互排斥变量.

唤醒条件变量等待

int pthread_cond_signal (pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

signal能够至少唤醒一个在使用cond条件变量的线程.普通情况下仅仅有一个线程返回,但偶尔也可能会因为假唤醒而导致一个以上的线程被唤醒.

broadcast就是广播的意思,所以这个是唤醒全部在cond上的线程.唤醒顺序肯定不是同一时候的.这个取决于线程的调度策略和优先级,也取决于他们又一次获得相连相互排斥变量时的竞争顺序.

这里提到了信号.事实上对于信号来说,发送是以进程为单位的.所以信号怎样影响线程是一个须要细致学习的事情,我在后面会试着具体写一些.

4.栅栏(屏障)

又看新书了..所以刚好有这个内容..原来大部分人都叫屏障...就我看的书叫栅栏...只是网上内容也非常少..基本用条件变量和相互排斥变量搭配就能够实现.所以我就不多说了.仅仅写下几个函数.

int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned count);

int pthread_barrier_wait(pthread_barrier_t *barrier);

int pthread_barrier_destroy(pthread_barrier_t *barrier);

简单来说,init中的count决定等待线程的个数.wait表示这个线程准备就绪,也就是count+1.然后destroy就是解除栅栏.

细节因为文章不多,我也不多写了.详细用到在man吧.

三.操作线程专有数据

除开全局变量和局部变量外,线程还能够拥有线程专属的数据,局部变量也算是私有数据了.感觉假设自己编写的时候注意一点,全然能够就用全局变量,感觉非常鸡肋...

当中还设置了构造和析构函数,于是帮助线程来分配私有空间和释放私有空间的作用..也就是C++中的构造和析构函数了...

线程专有数据是通过键-值的方式相应的.非常像C++里面的map.

创建和删除:

int pthread_key_create(pthread_key_t  *key,void (*destructor)(void *));

这个函数创建一个键值,通过key參数返回.这里还没有分配值空间,之后还要调用pthread_getspecific来分配空间.每一个键值仅仅能创建一次,所以不要用同一个key地址来多次创建.

第二个參数则是一个析构函数指针.一定要指向一个析构函数,不然就内存泄漏了.线程结束的时候会自己主动调用这个线程的.

为了避免不同键值被同一变量多次赋值.能够把创建函数封装在以下的函数的第二个參数来保证线程不会反复创建.

pthread_once_t once_control = PTHREAD_ONCE_INIT

int pthread_once(pthread_once_t *once_control ,void (*init_routine)(void ));

当中第一排的变量最好是一个全局变量,当然你用const定义也能够,但一定要初始化.

以下函数的第一个參数就是第一排定义的,保证仅仅被初始化一次.,第二个则是初始化函数.而且这个函数没有參数.但要注意,这个指针要指向一个初始化函数,这个初始化函数里面包括pthread_key_create这个函数.

我来举个样例:

void key_once_init(){

int rv;

   ......

   rv=pthread_key_create(&key(全局变量),NULL);

   .....

}

这样就设置好了初始化函数.key尽管设置的是全局变量,可是每一个线程来取出的值是不同的...感觉好鸡肋....仅仅是换个方式来保证不内存泄漏罢了...

而且这个函数还能够用来初始化全部动态初始化的对象.比方相互排斥变量,条件变量等..

int pthread_key_delete(pthread_key_t key);

这个就不用多说了.删除键.当然,这里要保证后面全部操作都不会再使用这个键才行.

注意,键值不是随意多个的.<limits.h>中包括一个宏PTHREAD_KEYS_MAX表示一个线程最多有多少个键值.

赋值:

int pthread_setspecific(pthread_key_t key ,const void *value);

void *pthread_getspecific(pthread_key_t key);

第一个函数是设置,第二个是获取值.

第一个函数的第二个參数,为了能分配各种类型的数据结构,通常是一个malloc返回的地址.

getspecific的key假设并没有之前设置setspecific的话,那么返回则是不确定的.就像未初始化的变量一样.

通过时钟来进行时间通知:

在Linux时间相关的文章中写了这个方式.就是通过定时器的方式

int timer_create(clockid_t clockid,struct sigevent *restrict evp,timer_t *restrict timerid);

也就是在第二个參数,struct sigevent中设置其成员:

int sigev_notify   通知类型

int sigev_signo    信号类型

union signval sigev_value    信号參数值

void (*)(union sigval)  sigev_notify_function   通知函数

(pthread_attr_t  *)  sigev_notify_attributes    通知属性

sigev_notify能够取值为

SIGEV_NONE    不生成异步信号

SIGEV_SIGNAL  生成排队的信号,并随信号携带一个应用定义的值,由于是排队的,所以一定是实时信号,或者说可靠信号.

SIGEV_THREAD  运行一个通知函数,这个是线程里面的高级使用方法,在线程那篇我会补充上的.

.

这里说的就是用SIGEV_THREAD这个宏.假设对sigev_notify设置了SIGEV_THREAD之后,那么就会使用西面两个成员.一个是线程的运行函数,一个是线程的属性.假设这么create之后,其异步事件来的时候,会创建一个新线程,属性为第二參数的sigev_notify_attributes,运行開始地址为sigev_notify_function.给线程開始函数的參数就是sigev_value.

而且在这个时候,sigev_signo能够不用写.这也是为什么我在上面写”异步事件”的原因

上一篇:13.1、多进程:进程锁Lock、信号量、事件


下一篇:分布式定时任务调度框架实践