Linux编程---线程

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

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

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

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

 

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

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

 

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

 

顺带一提erlang虚拟机下的轻量级线程似乎就是用户级线程.我记得在哪看过,erlang切换线程比c执行的线程切换还快.这里erlang的线程概念和用户级线程概念又不一样.erlang虚拟机自己实现的线程,叫做用户线程.(没有’级’字,操作系统概念还真是绕啊..).这是由于Linux只有LWP造成的.

 

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

 

--------------------补充说明分割线-----------------------

又看了些书.发现实际上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,其他线程是无法干扰的.实际上Linuxerrno是一个局部变量(这里也是网上查的,不过都是很老的帖子里面的,有错误请指正)

 

线程标识:

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之后才释放.

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

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

 

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

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

 

还有一点,线程是可以互相等待的.并不是只有主线程才能等待其他线程.任何线程都可以等待另外一个线程的结束.

 

4.分离线程

int pthread_detach(pthread_t thread);

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

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

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

 

注意,分离线程的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_ENABLEPTHREAD_CANCEL_DISABLE

type可以取值为PTHREAD_CANCEL_DEFERREDPTHREAD_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定义优先级范围,但自定义的范围一定要在minmax的返回值之间.

 

3)线程调度属性

--竞争范围属性

PTHREAD_SCOPE_PROCESSPTHREAD_SCOPE_SYSTEM

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

--继承属性

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

PTHREAD_INHERIT_SCHEDPTHREAD_EXPLICIT_SCHED

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

 

--调度策略属性

SCHED_FIFO

SCHED_RR

SCHED_OTHER

 

--调度参数属性(包含优先级)

这是一个对程序员不透明的结构体.具体的可以查看sched.h.但是至少包含一个sched_priority的成员.对于SCHED_FIFOSCHED_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.

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

 

 

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信号.sig0,kill()类似,只检查指定的线程是否存在.

 

3)等待信号

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

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

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

 

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

 

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

 

 

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

 

 

 

.线程同步

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

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

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下会正常返回,但是容易产生不容易发现的错误.初始化之后就不能改变其属性了.具体看下面.

 

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_setpsharedpthread_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);

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

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

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

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

 

条件变量属性

这里就只有进程共享属性.PTHREAD_PROCESS_PRIVATEPTHREAD_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返回的地址.

getspecifickey如果并没有之前设置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可以不用写.这也是为什么我在上面写”异步事件”的原因

 

 

 

Linux编程---线程,布布扣,bubuko.com

Linux编程---线程

上一篇:45个实用的JavaScript技巧、窍门和最佳实践


下一篇:Java解惑六:库之谜