目录

0.写在前面

在面试时总会被问到 进程和线程区别?(一些个人理解)

如何通过linux命令查看线程?

多线程如何实现

1.线程创建

参数:

返回值 RETURN VALUE

新线程以下列方式之一终止:

父子线程关系

什么时候线程创建出错呢?

特点

如何给第二个参数赋值?即如何设置线程属性?:默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级

【1.线程的分离状态】

【2.调度策略】

【3.调度参数】

【4.线程的继承性】

【5.是否绑定:线程的作用域】

【6.线程栈末尾的警戒缓冲区大小】

【7.线程栈地址】

【8.线程栈大小】

新线程的堆栈大小?

创建线程的数目没有限制吗?

2.pthread_join函数  阻塞等待线程退出,获取线程退出状态

功能

参数

返回值

示例:

1.对于创建线程属性 默认属性:joinable时,需调用pthread_jion()回收线程资源

2.对于属性为分离时,线程结束后,系统自动回收资源

3.pthread_detach函数  实现线程分离

功能

返回值

注意点

示例

4.线程终止、资源回收

可结合的(jionable)线程的几种退出方式

1. 子线程使用return退出,主线程中使用pthread_join回收线程(线程创建部分已讲到)

2.子线程使用pthread_exit退出,主线程中使用pthread_join接收pthread_exit的返回值,并回收线程

*3.主线程中调用pthread_cancel,然后调用pthread_join回收线程

pthread_cancel()函数讲解

pthread_testcancel()函数讲解

pthread_setcancelstate()、pthread_setcanceltype函数讲解

示例:

5.线程同步、线程间通信

什么是线程同步?什么是线程通信?

线程同步的方式和机制

【1】互斥锁

初始化锁。

互斥锁属性:

示例:

【2】条件变量

1.初始化条件变量

2.有两个等待函数

3、条件变量原则

4.示例:

【3】信号量

1.无名信号量

2.有名信号量



0.写在前面

有关进程讲解另一篇博客:https://blog.csdn.net/Wmll1234567/article/details/114653213?spm=1001.2014.3001.5501

这篇博客详细介绍了进程切换的一些过程,这将帮助理解线程、进程的区别(主要就是切换的时候的区别)

文中所有实验环境:电脑主存:8g   虚拟内存大小:13181MB   编译环境 win10 codeblock  GCC

  • 在面试时总会被问到 进程和线程区别?(一些个人理解)

进程:操作系统为正在运行的程序提供的抽象,可单独运行  ,进程间是互不影响的,有自己独立的虚拟内存空间,一个进程挂掉,在保护模式下不会影响其他进程崩溃。

操作系统以进程为单位去分配操作系统资源(cpu,内存等),资源分配的基本单位,又是调度运行的基本单位

线程:也是正在运行的程序,只不过必须依赖于进程,由进程去创建(共享进程的内存空间:不独立拥有资 源,只能访问隶属于进程的资源)(所以进程挂掉,那么其他所有产生的线程也会死                             掉),是进程需要并发执行多个任务的一种机制。

                   线程是进程中执行运算的最小单位,即执行处理机调度的基本单位,是进程中的一个执行流

        如果想要一段程序同时干很多任务,我们会有两种操作:多进程、多线程

        多进程:linux 下使用fork函数创建,这种多进程的创建相当于复制,本身有一条河流往前走,再复制一份相同的河流,互不干扰往前走。

        多线程:linux 使用 pthread_creat函数创建相当只是把一条河流截成很多条小溪,没有额外的拷贝动作,因此造就了一大优点:系统开销小

【进程切换分两步】:

1.切换页目录以使用新的地址空间

2.切换内核栈和硬件上下文

对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

         切换的性能消耗:

1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))会被全部刷新(支持ASID另说),这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题

  • 如何通过linux命令查看线程?

  1. ps -t -p pid

  1. pstree -p  pid 

多线程如何实现

主要是CPU通过给每个线程分配CPU时间片来实现多线程的。即使是单核处理器(CPU)也可以执行多线程处理。

时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)

1.线程创建

 #include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread
  • 参数:

【thread】:               要创建的线程的线程标识符指针(pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:typedef unsigned long int pthread_t;它是一个线程的标识符)

【attr】:                 创建线程时使用时候确定新线程的属性;使用以下方法初始化此结构pthread_attr_init和相关函数   

【第三个参数】:     返回值是void类型的指针函数,也就是线程运行函数的起始地址

【第四个参数 arg】线程运行函数的形参

  • 返回值 RETURN VALUE

On  success,  pthread_create()  returns 0;

on error, it returns an error number, and the contents of *thread are undefined

  • 新线程以下列方式之一终止:

*它调用pthread_exit(3)

*它从start_routine()返回。这等效于使用值调用pthread_exit(3)

*它被取消(请参阅pthread_cancel)。

*主线程从main()返回。这导致进程中所有线程的终止。

  • 父子线程关系

新线程继承了创建线程的信号掩码(pthread_sigmask(3))的副本。套装
       新线程的待处理信号中的值为空(sigpending(2))。新线程不会继承创建线程的备用信号堆栈(sigaltstack(2))

新线程继承了调用线程的浮点环境(fenv(3))。

新线程的CPU时间时钟的初始值为0(请参阅pthread_getcpuclockid(3))

  • 什么时候线程创建出错呢?

EAGAIN资源不足,无法创建另一个线程,

系统施加的限制数量遇到线程。后一种情况可能以两种方式发生:RLIMIT_NPROC软资源限制(通过setrlimit(2)设置),用于限制实际用户ID的进程数到达;或内核在系统范围内对线程数的限制,/ proc / sys / ker‐达到了nel / threads-max。

EINVAL attr中的设置无效。

EPERM无权设置attr中指定的调度策略和参数

  • 特点

除非采用实时调度策略,否则在调用pthread_create()之后,不确定哪个线程(调用者线程或新线程)接下来将执行。

  • 如何给第二个参数赋值?即如何设置线程属性?:默认的属性为非绑定非分离缺省1M的堆栈与父进程同样级别的优先级

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。

 #include <pthread.h>int pthread_attr_init(pthread_attr_t *attr);初始化int pthread_attr_destroy(pthread_attr_t *attr);去初始化属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义
typedef struct
{int detachstate;1.线程的分离状态int  schedpolicy;2.线程调度策略struct  sched_param schedparam;3.线程的调度参数int inheritsched;4.线程的继承性int scope;5.线程的作用域size_t guardsize;6.线程栈末尾的警戒缓冲区大小int  stackaddr_set;void*  stackaddr;7.线程栈的位置size_t   stacksize; 8线程栈的大小
}pthread_attr_t;

【1.线程的分离状态】

         【两种属性:joinable(非分离)   detached(分离)】 默认情况下,线程属性为:joinable

                1. joinable   :则另一个线程可以调用 pthread_join 等待新线程终止并获取其退出状态, 这种情况下,只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
                2. detached:当分离的线程终止时,其资源会自动释放回系统,无法与该线程联接以获得其退出状态。设置线程分离对于某些守护类型程序线程非常有用,这些线程的退出状态应用程序                      不需要关心什么,设置线程处于分离状态(使用pthread_attr_setdetachstate)。分离属性在网络通讯中使用的较多

注意: 1.在joinable下创建的线程最终应使用pthread_join    或使用pthread_detach分离;

2.在detached分离状态下创建的线程,调用pthread_detach或pthread_join是错误的

3.如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用                               pthread_create的线程就得到了错误的线程号。(如何解决:1.最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函                           数pthread_create返回;)

#include <pthread.h>int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);参数:attr:线程属性变量detachstate:分离状态属性PTHREAD_CREATE_DETACHED 分离状态启动PTHREAD_CREATE_JOINABLE   正常启动线程
成功返回0,失败返回-1

【2.调度策略】

schedpolicy决定线程将以什么调度策略被调度。linux下的调度策略有三种:
          SCHED_OTHER 分时调度
          SCHED_FIFO 实时调度(先进先出)
          SCHED_RR 实时调度(公平轮转法)

#include <pthread.h>
int pthread_attr_getschedpolicy(const pthread_attr_t * attr, int * policy);
int pthread_attrsetschedpolicy(pthread_attr_t * attr, int plicy);
参数: attr :线程参数属性policy:调度策略     SCHED_OTHER 分时调SCHED_FIFO  实时调度(先进先出)SCHED_RR  实时调度(公平轮转法)
成功返回0,失败返回-1

【3.调度参数】

schedparam决定调度策略中的一些参数选择,目前一般仅支持设备优先级。在头文件中的定义如下:

struct sched_param
{int sched_priority;参数的本质就是优先级
}
使用方式如下:
#include <pthread.h>
int pthread_attr_getschedparam(const pthread_attr_t * attr, sched_param* param);
int pthread_attr_setschedparam(pthread_attr_t * attr,sched_param * param);
参数: attr :线程参数属性param: sched_param结构体
成功返回0,失败返回-1.
注意:系统提供了获取最大优先级和最小优先级的接口
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);
参数: policy:调度策略,SCHED_OTHER,SCHED_FIFO,SCHED_RRreturn   对应调度策略最大或最小的优先级
注意:实时的调度策略的优先级范围是1~99.分时的调度策略的优先级范围是0

【4.线程的继承性】

inheritsched决定子线程的调度策略和调度参数使用的是attr参数里面的,还是默认继承父线程的。我们设置实时策略(SCHED_RR)不成功就是因为这个原因。由于默认是继承父线程的属性,而父线程的属性默认为分时调度。导致设置失败。使用方式如下:

#include <pthread.h>
int pthread_attr_getinheritsched(const pthread_attr_t * attr,int * inheritsched);
int pthread_attr_setinheritsched(pthread_attr_t * attr,int inheritsched);
参数: attr :线程参数属性inheritsched:线程的继承性PTHREAD_INHERIT_SCHED  新的线程继承创建线程的策略和调度参数PTHREAD_EXPLICIT_SCHED 新的线程使用attr中的调度策略和调度参数
成功返回0,失败返回-1

【5.是否绑定:线程的作用域】

关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

scope决定线程的作用域,也就是说和哪些线程竞争资源。可以设置为同进程内竞争资源,也可以设置为系统内竞争资源。如果是同进程内竞争资源,当正在运行的线程优先级不高但不是同进程里面的线程,而该线程的优先级较高并且是实时策略。也是无法进行抢占资源的。反之如果是系统内竞争资源,就可以抢占

设置线程绑定状态的函数为pthread_attr_setscope

 int pthread_attr_setscope(pthread_attr_t *attr, int scope);int pthread_attr_getscope(pthread_attr_t *attr, int *scope);参数:attr :线程参数属性scope:线程的作用域PTHREAD_SCOPE_PROCESS  进程内竞争资源PTHREAD_SCOPE_SYSTEM   系统级竞争资源eg:
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
pthread_attr_init(&attr);/*初始化属性值,均设为默认值*/
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);//设置为绑定属性pthread_create(&tid, &attr, (void *) my_function, NULL);

【6.线程栈末尾的警戒缓冲区大小】

guardsize决定线程栈末尾之后以避免栈溢出的扩展内存大小。我们知道当我们局部变量较多占用空间超过栈空间大小时,就会发生coredump。而该值是保证超过多少以内不会抛出异常的保护,相当于扩容

#include<pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t * attr,size_t*guardsize);
int pthread_attr_setguardsize(pthread_attr_t * attr,size_t * guardsize);
参数:attr :线程参数属性guardsize:警戒缓冲区的大小
成功返回0,失败返回-1.

【7.线程栈地址】

stackaddr决定栈空间的地址,具体用在什么方面,我也不清楚,目前知道需要注意的是:设定的栈地址必须以linux页面大小对齐。使用如下

#include<pthread.h>
int pthread_attr_getstack(const pthread_attr_t * attr,void * stackaddr);
int pthread_attr_setstackaddr(pthread_attr_t * attr,void * stackaddr);
参数:attr :线程参数属性stackaddr:栈地址
成功返回0,失败返回-1.

【8.线程栈大小】

stacksize决定线程栈的大小。因为我们默认线程创建之后,线程栈的大小,默认为8M,这对于内存比较紧张的嵌入式平台而言是巨大的浪费。所以我们需要对线程的业务需求设置合适的栈空间大小。因为当我们的局部变量占用空间超过栈空间,就会引起coredump。栈最小为16kb。使用如下:

#include<pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t * attr,size_t * stacksize);
int pthread_attr_setsstacksize( pthread_attr_t * attr,size_t * stacksize);
参数:attr :线程参数属性stacksize:堆栈大小:(以字节为单位)
成功返回0,失败返回-1.错误pthread_attr_setstacksize()可能因以下错误而失败:EINVAL堆栈大小小于PTHREAD_STACK_MIN(16384)字节。在某些系统上,如果stacksize不是系统页面大小的倍数,pthread_attr_setstacksize()可能会失败,并显示错误EINVAL。
  • 新线程的堆栈大小?

在Linux / x86-32上,新线程的默认堆栈大小为2 MB。在NPTL线程下实现,如果程序启动时的RLIMIT_STACK软资源限制为 值,而不是“ unlimited”,则它确定新线程的默认堆栈大小。使用
       pthread_attr_setstacksize,可以在attr参数中显式设置堆栈大小属性用于创建线程,以获取默认值以外的堆栈大小。

  • 创建线程的数目没有限制吗?

我们知道线程依赖于进程,而进程各自拥有独立的虚拟内存空间,因此,能开多少个线程也就与进程的虚拟内存空间大小有关(一般是运存的1.5~2倍大小,不过也可以自定义大小)

假设进程的虚拟内存大小为2g, 一个线程的栈要预留1M的内存空间(也可自定义大小);那么理论上最多能创建2048个线程(但是内存不可能完全拿来作线程栈没所以实际数目比这个要小)

以下实验本机电脑 主存:8g   虚拟内存大小:13181MB   编译环境 win10 codeblock  GCC

2.pthread_join函数  阻塞等待线程退出,获取线程退出状态 

 #include <pthread.h>int pthread_join(pthread_t thread, void **retval);
  • 功能

一个线程的终止对于另外一个线程而言是一种异步的事件,有时我们想等待某个ID的线程终止了再去执行某些操作,pthread_join函数为我们提供了这种功能,该功能称为线程的连接.

当线程 X 连接线程 Y 时,如果线程 Y 仍在运行,则线程 X 会阻塞直到线程 Y 终止;如果线程 Y 在被连接之前已经终止了,那么线程 X 的连接调用会立即返回。

连接线程其实还有另外一层意义,一个线程终止后,如果没有人对它进行连接,那么该终止线程占用的资源,系统将无法回收,而该终止线程也会成为僵尸线程。因此,当我们去连接某个线程时,其实也是在告诉系统该终止线程的资源可以回收了

注意:

  1. 一个线程只能被一个线程所连接。如果多个线程同时尝试与同一线程联接,则结果是不确定的(使用先前已连接的线程进行连接会导致未定义的行为)
  2. 被连接的线程必须是非分离joinable的,否则连接会出错。如果取消调用pthread_join()的线程,则目标线程将保持可连接状态
  3. 用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
  4. 对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
  5. 在线程设置为joinable后,可以调用pthread_detach()使之成为detached。但是相反的操作则不可以。还有,如果线程已经调用pthread_join()后,则再调用pthread_detach()则不会有任何效果。
  • 参数

  1. thread:等待退出线程的线程号。

  2. value_ptr:退出线程的返回值。

  • 返回值

成功时:返回0;否则,返回0。如果出错,则返回错误号。

错误
       EDEADLK:检测到死锁(例如,两个线程试图相互连接);或线程指定 结束调用线程。

EINVAL线程不是可连接的线程。

EINVAL另一个线程已经在等待与此线程的加入。

ESRCH找不到具有ID线程的线程。

如果retval不为NULL,则pthread_join()复制目标线程的退出状态(即目标线程提供给pthread_exit的值到* retval指向的位置。如果取消了目标线程,则将PTHREAD_CANCELED放在* retval中。

示例:

1.对于创建线程属性 默认属性:joinable时,需调用pthread_jion()回收线程资源

因为: linux线程执行和windows不同,pthread有两种状态joinable状态和unjoinable状态,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放

下面的程序只是个示例,如果一个程序是一直运行着的,即主线程不会无故退出,那么对于线程资源回收就一定要去处理:Linux系统中程序的线程资源是有限的,表现为对于一个程序其能同时运行的线程数是有限的。而默认的条件下,一个线程结束后,其对应的资源不会被释放,于是,如果在一个程序中,反复建立线程,而线程又默认的退出,则最终线程资源耗尽,进程将不再能建立新的线程。

 

2.对于属性为分离时,线程结束后,系统自动回收资源

3.pthread_detach函数  实现线程分离

#include <pthread.h>
int pthread_detach(pthread_t thread);
  • 功能

pthread_detach()函数将由线程标识的线程标记为已分离。当分离的线程终止时,其资源会自动释放回系统,而无需另一个线程与终止的线程联接。

  • 返回值

成功时,pthread_detach()返回0;否则,返回0。如果出错,则返回错误号。

错误
       EINVAL线程不是可连接的线程。

ESRCH找不到具有ID线程的线程

  • 注意点

尝试分离已经分离的线程会导致未指定的行为。

分离线程后,将无法将其与pthread_join(3)联接或使其再次可联接。

  • 示例

pthread_detach(pthread_self());

有两种方式创建分离线程:

(1)在线程创建时将其属性设为分离状态(detached);之前在讲线程创建,设置属性时已详细讲到

(2)在线程创建后将其属性设为分离的(detached):

【1】在线程中调用pthread_detach(pthread_self());(被创建的子线程也可以自己分离自己,子线程调用pthread_detach(pthread_self())就是分离自己,因为pthread_self()这个函数返回的就是自己本身的线程ID)

         【2】主线程中调用pthread_detach(pid),pid为子线程的线程号

设置分离属性,我们可以看到,主线程不会再等待新线程结束,会各自独立执行。新线程结束后由系统自动回收资源

#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>static void *thread_start()
{char * str = NULL;while(1){sleep(1);}printf("thread end\n");
}
#define MAX_TID 5000
int main(int argc, char *argv[])
{int ret = -1;int i = 0;pthread_attr_t attr;pthread_t tid;pthread_attr_init(&attr);//初始化线程属性//pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置线程分离属性ret = pthread_create(&tid,&attr,(void *)thread_start,NULL);if(ret!=0){printf("creat_thread failed : %d\n",ret);perror("error:");}ret = pthread_detach(tid);  // 使线程处于分离状态if(ret!=0){printf("pthread_detach failed : %d\n",ret);perror("error:");}//ret = pthread_join(tid,NULL)//已经设置分离状态,不能再次加入,会报错printf("main end\n");return 0;
}

4.线程终止、资源回收

其实前面我们已经讲的很清楚了。只有线程属性为可结合的(非分离属性时),才需要考虑去回收资源。。。。。非分离属性的,线程结束后系统自动回收,除非这个线程一直不能结束

可结合的(jionable)线程的几种退出方式

1. 子线程使用return退出,主线程中使用pthread_join回收线程(线程创建部分已讲到)

2.子线程使用pthread_exit退出,主线程中使用pthread_join接收pthread_exit的返回值,并回收线程

*3.主线程中调用pthread_cancel,然后调用pthread_join回收线程

   注意:在要杀死子线程对应的处理函数的内部

      pthread_cancel函数执行的条件:

      1、产生了系统调用(sleep、read、write、open等系统接口)

      2、pthread_testcancel();//设置取消点

  • pthread_cancel()函数讲解

 #include <pthread.h>int pthread_cancel(pthread_t thread);

功能描述:取消请求发送到线程线程。目标线程是否以及何时对取消请求作出反应取决于以下两个属性:【该线程的控制权:其可取消状态类型

【1】由pthread_setcancelstate 确定的线程的可取消状态可以启用(默认设置)或禁用。如果线程已禁用取消,则取消请求将一直排队,直到该线程启用取消为止。如果线程已启用取消,则其取消类型将确定何时发生取消。

【2】由pthread_setcanceltype(确定的线程取消类型可以是异步的也可以是延迟的(默认设置)。异步可取消性意味着可以随时(通常立即立即取消线程,但系统不对此进行保证)。延迟可取消性意味着取消将被延迟,直到线程下一次调用作为取消点的函数为止。【 pthreads中提供了可能是或可能是取消点的函数列表】

当执行请求的取消操作时,线程将按照以下顺序执行以下步骤:

1.取消清除处理程序被弹出(与它们被推的顺序相反)并被调用。

2.以未指定的顺序调用特定于线程的数据析构函数。

3.线程终止。

上述步骤相对于pthread_cancel()调用是异步发生的; pthread_cancel()的返回状态仅通知调用方取消请求是否已成功排队。

取消的线程终止后,使用pthread_join(3)与该线程的联接获得PTHREAD_CANCELED作为线程的退出状态。 (加入线程是知道取消已完成的唯一方法。)

  返回值:成功:0     失败:非 0

  注意:在Linux上,取消是使用信号实现的。在NPTL线程实现下,第一实时信号(即信号32)用于此目的。在LinuxThreads上,如果有实时信号可用,则使用第二个实时信号,否则使用SIGUSR2

  • pthread_testcancel()函数讲解

  #include <pthread.h>void pthread_testcancel(void);线程取消功能处于启用状态且取消状态设置为延迟状态时,pthread_testcancel()函数有效。

功能

调用pthread_testcancel()会在调用线程中创建一个取消点,以便正在执行不包含取消点的代码的线程将响应取消请求。

如果禁用了取消功能(使用pthread_setcancelstate),或者没有取消请求待处理,则对pthread_testcancel()的调用无效。

  • pthread_setcancelstate()、pthread_setcanceltype函数讲解

 #include <pthread.h>int pthread_setcancelstate(int state, int *oldstate);功能:设置本线程对Cancel信号的反应,是否可被取消
参数:state:PTHREAD_CANCEL_ENABLE(缺省)PTHREAD_CANCEL_DISABLE: 取消请求会被放入队列。直到目标线程的cancel state变为 PTHREAD_CANCEL_ENABLE,取消请求才会从队列里取出,发到 目标线程 old_state:如果不为NULL则存入原来的Cancel状态以便恢复。int pthread_setcanceltype(int type, int *oldtype);功能:设置本线程取消动作的执行时机,仅当Cancel状态为Enable时有效
参数:type:PTHREAD_CANCEL_DEFERRED :取消请求被延迟,下一个取消点退出(同步取消)PTHREAD_CANCEL_ASYNCHRONOUS:立马退出(该线程可以随时取消。 (通常,它将在收到取消请求 后立即被取消,但系统不对此予以保证。))(异步取消)oldtype如果不为NULL则存入运来的取消动作类型值

示例:

【1】.如果不对pthread_cancel()请求取消响应属性设置。默认:启用取消、延迟的(意思是并不是立马结束:必须遇到系统调用、或者遇到了取消点)

以下可以看出线程child里没遇到系统调用,也没有取消点。因此,即使主线程调用pthread_cancel,线程仍旧未结束

所以,记得设置取消点:设置 pthread_testcancle()。如下:

【2】.如果不对pthread_cancel()请求取消响应属性设置。默认:启用取消、延迟的(意思是并不是立马结束:必须遇到系统调用、或者遇到了取消点)

以下设置了取消点,可以看到线程结束成功

【3】pthread_setcancelstate() 可以用来实现某一段代码忽略取消请求信号,随后再恢复即可

【4】https://linux.die.net/man/3/pthread_setcanceltype     pthread_setcanceltype()

一般来说,线程终止有两种情况:正常终止和非正常终止。

1.线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;

2.非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。 
          最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程

用PTHREAD_CANCEL_DEFERRED取消方式是因为线程可能在获取临界资源后(如获取锁),未释放资源前收到退出信号,如果使用PTHREAD_CANCEL_ ASYNCHRONOUS的方式,无论线程运行到哪个位置,都会马上退出,而占有的资源却得不到释放。

采用PTHREAD_CANCEL_DEFERRED取消方式,线程需要运行到取消点才退出,而主线程在调用pthread_cancel后,不能马上进行线程资源释放,必须调用pthread_join进入休眠,直至等待指定线程退出。

使用PTHREAD_CANCEL_DEFERRED方式并不能完全避免这个问题,因为无法保证在获取临界资源后(比如lock操作)不会进行可以作为取消点的操作(如进行sleep),此时主线程如果对该线程发送cancel信号,线程将会在不释放锁的情况下直接结束运行,即还是会出现在释放资源前线程就退出的问题。

为了避免上述情况,不仅需要设置可取消类型,还需要设置可取消状态。将获取临界资源-释放临界资源之间的代码块都设置成PTHREAD_CANCEL_DISABLE状态,其余的代码块都设置成PTHREAD_CANCEL_ENABLE状态,确保线程在安全的地方退出。如果在可以安全退出的代码块不存在取消点系统调用,可以调用pthread_testcancel函数自己添加取消点。

  • 这里先介绍两个函数,示例等统一把线程同步、线程通信讲完,再演示会应更清晰
 #include <pthread.h>这些函数操纵调用线程的线程取消清理处理程序的堆栈。清理处理程序是在取消线程时(或在以下所述的各种其他情况下)自动执行的功能;例如,它可以解锁互斥锁,以便该互斥锁可用于该进程中的其他线程void pthread_cleanup_push(void (*routine)(void *),void *arg);函数将例程推入清理处理程序堆栈的顶部。当例程在以后被调用时,它将被赋予arg作为其参数。void pthread_cleanup_pop(int execute);函数会删除清理处理程序堆栈顶部的例程,并且如果execute为非零,则可以选择执行该例程在以下情况下,取消清除处理程序将从堆栈中弹出并执行:1.取消线程时,将弹出所有堆叠的清理处理程序并以与将它们推入堆栈的顺序相反的顺序执行。2.当线程通过调用pthread_exit(3)终止时,所有清理处理程序均如前所述执行。 (如果线程通过执行从线程启动函数返回的操作而终止,则不会调用清理处理程序。)3.当线程使用非零执行参数调用pthread_cleanup_pop()时,将弹出并执行最顶层的清除处理程序。

总的来说就是:一个是线程没有cancel、exit意外终止话一直运行到pop函数那里,此时pop函数的execute设置为0,只会从清理函数堆栈取一个清理函数出来进行删除,不会调用执行,若execute设置为非0就会从清理函数堆栈中取出一个清理函数,并调用执行,执行完后删除。若一个线程有cancel、exit意外被终止,不管你那个execute有没有设置为0,系统都会自动从清理函数堆栈中取出所有的清理函数进行调用执行,并执行完后进行删除

注意

1.pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是pthread.h中的宏定义:

#define pthread_cleanup_push(routine,arg)                                     { struct _pthread_cleanup_buffer _buffer;                                   _pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute)                                          _pthread_cleanup_pop (&_buffer, (execute)); }
可见,pthread_cleanup_push()带有一个"{",而pthread_cleanup_pop()带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译

2.这两个函数对中,最好不要调用return,运用不当引起段错误

  • 示例:

5.线程同步、线程间通信

这里可以看这篇博客,讲解了进程间通信、同步。可以做一下对比学习

https://blog.csdn.net/Wmll1234567/article/details/114653213?spm=1001.2014.3001.5501

什么是线程同步?什么是线程通信?

这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步(下文统称为同步)

通信:两个线程间传递交互数据

线程间的通信有两种情况:

  1、一个进程中的线程与另外一个进程中的线程通信,由于两个线程只能访问自己所属进程的地址空间和资源,故等同于进程间的通信。

  2、同一个进程中的两个线程进行通信。

关于进程间通信(IPC)可以看我的另一篇博文:https://blog.csdn.net/Wmll1234567/article/details/114653213?spm=1001.2014.3001.5501

  比起进程复杂的通信机制(管道、匿名管道、消息队列、信号量、共享内存、内存映射以及socket等),线程间通信要简单的多。

  因为同一进程的不同线程共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段,所以线程之间可以方便、快速地共享信息。只需要将数据复制到共享(全局或堆)变量中即可。不过,要避免出现多个线程试图同时修改同一份信息

线程安全:

  所在的进程中有多个线程在同时运行,而这些线程可能会同时某一段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全就是说多线程访问同一段代码不会产生不确定的结果。编写线程安全的代码依靠线程同步。

线程间的同步:

  如果变量只读时,多个线程同时读取该变量不会有一致性问题,但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。 

  • 同步和通信其实分割来说是不好的,因为他们是相辅相成的,必要时把他们理解为一个东西可能更好。

线程同步的方式和机制

  • linux下提供了多种方式来处理线程同步,最常用的是互斥锁条件变量信号量

【1】互斥锁

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象(也可是代码块)。

  • 初始化锁。

在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。可以用静态和动态两种j方式初始化:

  • 静态分配:POSIX定义了一个宏来静态初始化互斥锁,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 动态分配 :采用 pthread_mutex_init() 函数来初始化互斥锁,中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。
 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);
  • 互斥锁静态初始化和动态初始化的区别?
  • 互斥锁属性:

//初始化互斥锁属性
pthread_mutexattr_init(pthread_mutexattr_t attr);//销毁互斥锁属性
pthread_mutexattr_destroy(pthread_mutexattr_t attr);//用于获取互斥锁属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr , int *restrict pshared);//用于设置互斥锁属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr , int pshared);参数:
attr表示互斥锁的属性pshared表示互斥锁的共享属性,由两种取值:1)PTHREAD_PROCESS_PRIVATE:锁只能用于一个进程内部的两个线程进行互斥(默认情况)2)PTHREAD_PROCESS_SHARED:锁可用于两个不同进程中的线程进行互斥,使用时还需要在进程共享内存中分配互斥锁,然后为该互斥锁指定属性就可以了
  • 互斥锁的分类:
//获取互斥锁类型
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr , int *restrict type);//设置互斥锁类型
int pthread_mutexattr_settype(const pthread_mutexattr_t *restrict attr , int type);参数type表示互斥锁的类型,总共有以下四种类型1.PTHREAD_MUTEX_NOMAL:标准互斥锁,第一次上锁成功,第二次上锁会失败并阻塞2.PTHREAD_MUTEX_RECURSIVE:递归互斥锁,第一次上锁成功,第二次上锁还是会成功,可以理解为内部有一个计数器,每加一次锁计数器加1,解锁减13.PTHREAD_MUTEX_ERRORCHECK:检查互斥锁,第一次上锁会成功,第二次上锁出错返回错误信息,不会阻塞4.PTHREAD_MUTEX_DEFAULT:默认互斥锁,第一次上锁会成功,第二次上锁会失败

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

  • 普通加锁:对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。
 int pthread_mutex_lock(pthread_mutex *mutex);
  • 测试加锁:pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
 int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
 int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 销毁锁。锁在是使用完成后,需要进行销毁以释放资源。
 int pthread_mutex_destroy(pthread_mutex *mutex);
  • 加锁与解锁

lock 与 unlock:

lock 尝试加锁,如果锁不成功,线程阻塞,阻塞到持有互斥量的线程解锁为止。

unlock 主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞,先唤醒。

例如:T1 T2 T3 T4  使用一把 互斥锁。T1 加锁成功,其他线程均阻塞,直至 T1 解锁。T1 解锁后,T2,T3,T4均被唤醒,并自动再次尝试加锁。

lock 与 trylock:

lock 加锁失败会阻塞,等待锁释放。

trylock 加锁失败直接返回错误号(如:EBUSY),不阻塞。

示例:

1.int  pthread_mutex_trylock(&mutex):测试加锁函数在锁已经被占据时返回EBUSY而不是挂起等待,当然,如果锁没有被占领的话可以获得锁

为了清楚的看到两个线程争用资源的情况,我们使得其中一个函数使用测试加锁函数进行加锁,而另外一个使用正常的加锁函数进行加锁。


#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
int ticket_sum=20;
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;//static init mutexvoid *sell_ticket_1(void *arg){int i;for(i=0; i<20; i++){pthread_mutex_lock(&mutex_x);if(ticket_sum>0){sleep(1);printf("thread_1 sell the %d th ticket\n",20-ticket_sum+1);ticket_sum--;}pthread_mutex_unlock(&mutex_x);sleep(1);}return 0;
}void *sell_ticket_2(void *arg){int flag;int i;for(i=0; i<20; i++){flag=pthread_mutex_trylock(&mutex_x);if(flag==EBUSY){printf("sell_ticket_2:the variable is locked by sell_ticket_1\n");}else if(flag==0){if(ticket_sum>0){sleep(1);printf("thread_2 sell the %d th ticket\n",20-ticket_sum+1);ticket_sum--;}pthread_mutex_unlock(&mutex_x);}sleep(1);}return 0;
}
int main()
{int flag;pthread_t tids[2];flag=pthread_create(&tids[0],NULL,&sell_ticket_1,NULL);if(flag){printf("pthread create error ,flag= %d\n",flag);return flag;}flag=pthread_create(&tids[1],NULL,&sell_ticket_2,NULL);if(flag){printf("pthread create error ,flag= %d\n",flag);return flag;}void *ans;sleep(30);flag=pthread_join(tids[0],&ans);if(flag){printf("tid=%d  ,join erro flag=%d\n",tids[0],flag);return flag;}else{printf("ans=%d \n",ans);}flag=pthread_join(tids[1],&ans);if(flag){printf("tid=%d  ,join erro flag=%d\n",tids[1],flag);return flag;}else{printf("ans= %d\n",ans);}return 0;
}

我们可以看到明显线程1抢占了更多的资源,为什么呢?

2.两个线程都用的是正常加锁函数,测试结果如下:可以看到执行情况大概是一半一半。


#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
int ticket_sum=20;
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;//static init mutexvoid *sell_ticket_1(void *arg)
{int i;for(i=0; i<20; i++){pthread_mutex_lock(&mutex_x);if(ticket_sum>0){sleep(1);printf("thread_1 sell the %d th ticket\n",20-ticket_sum+1);ticket_sum--;}pthread_mutex_unlock(&mutex_x);sleep(1);}return 0;
}void *sell_ticket_2(void *arg)
{int flag;int i;for(i=0; i<20; i++){pthread_mutex_lock(&mutex_x);if(ticket_sum>0){sleep(1);printf("thread_2 sell the %d th ticket\n",20-ticket_sum+1);ticket_sum--;}pthread_mutex_unlock(&mutex_x);sleep(1);}return 0;
}
int main()
{int flag;pthread_t tids[2];flag=pthread_create(&tids[0],NULL,&sell_ticket_1,NULL);if(flag){printf("pthread create error ,flag= %d\n",flag);return flag;}flag=pthread_create(&tids[1],NULL,&sell_ticket_2,NULL);if(flag){printf("pthread create error ,flag= %d\n",flag);return flag;}void *ans;sleep(30);flag=pthread_join(tids[0],&ans);if(flag){printf("tid=%d  ,join erro flag=%d\n",tids[0],flag);return flag;}else{printf("ans=%d \n",ans);}flag=pthread_join(tids[1],&ans);if(flag){printf("tid=%d  ,join erro flag=%d\n",tids[1],flag);return flag;}else{printf("ans= %d\n",ans);}return 0;
}

如果将上面程序做如下改动,可以看到:没有使用sleep的线程2 几乎瞬间抢到所有资源

再如下:线程的切换速度很快,所以,如果两个线程整个执行时长相差不大,他们争夺的资源是平均的。所以非必要,不要使用sleep()

总结:

1、不要去解锁一个未被加锁的mutex锁;

2、不要一个线程中加锁而在另一个线程中解锁;

3、使用mutex锁用于保护临界资源,严格按照“加锁-->写入/读取临界资源-->解锁”的流程执行,对于线程间同步的需求使用条件变量或信号量实现。

4、所有线程执行到lock()的时候,会互斥的访问接下来的代码,在这段代码执行到unlock()之前,其他线程执行到lock()会进入阻塞直到unlock()后,进入就绪,等待操作系统调用。
需要注意的是,lock和unlock都会导致较大的计算机资源开销,尽量使需要互斥的代码简短快速。

5、需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互 斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数 pthread_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信 息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点
6.缺陷定义 :  以上描述场景,实际上是锁的一个经典的使用场景。程序在获取了临界资源后异常退出,临界资源一直处于加锁状态,其他进程/线程申请锁程序未正常处理就会导致阻塞,甚至死锁等待。

缺陷修复 :  这类缺陷主要产生的问题就是,后续进程在申请锁时,如果出现了“锁被已死进程”占有后,应该怎么让系统回收锁的问题。本质上这个问题就是进程间互斥锁回收问题

【2】条件变量

  • 与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥量同时使用
  • 条件变量是一种“事件通知机制”,它本身不提供、也不能够实现“互斥”的功能。因此,条件变量通常(也必须)配合互斥量来一起使用,其中互斥量实现对“共享数据”的互斥(即同步),而条件变量则去执行 “通知共享数据状态信息的变化”的任务。比如通知队列为空、非空,或任何其他需要由线程处理的共享数据的状态变化。实际开发中,生产者-消费者模型是经常被使用到的一个技巧,若干线程不断的往某个队列中生产数据进去,而其他若干线程不断的去队列中消费数据。这里“队列”是共享的数据,需要用互斥量来对其进行加锁,防止数据紊乱;细心的读者发现,去队列消费数据的线程怎么才能知道队列中何时有数据进来,总不能一直在那里傻傻的等待吧!如果一直等待,则需要互斥量加锁,那么生产者线程会加锁失败,会一直尝试去加锁。这无形中会导致性能的急剧下降。因此这时候是“条件变量”闪亮登场,展示它真正“技术”的时候了,它会通知对应的(或广播所有等待状态变化的)线程,告知它们这个“共享数据”的状态变化信息,以执行对应的处理
  • 使用条件变量之前要先进行初始化

条件变量分为两部分: 条件和变量。

  • 条件本身是由互斥量保护的.。
  • 线程在改变条件状态前先要锁住互斥量.。
  • 它利用线程间共享的全局变量进行同步的一种机制。

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标准中为条件变量定义了属性,但在Linux中没有实现,因此cond_attr值通常为NULL,且被忽略。

2.有两个等待函数

  • (1)无条件等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
函数执行时先自动释放指定的互斥锁,然后等待条件变量的变化;在函数调用返回之前(即wait成功获得cond条件的时候),会自动将指定的互斥量重新锁住(即在“等待的条件变量满足条件时,会重新锁住指定的锁”)
  • (2)计时等待 
 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
  1. 如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
  2. 无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 请求 竞争条件 。mutex互斥锁必须是普通锁 或者适应锁
  3. 且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
  • 激发条件

(1)激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)    

 int pthread_cond_signal(pthread_cond_t *cond);
函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,
继续执行(通过条件变量cond发送消息,若多个消息在等待,它只唤醒一个)。如果没有
线程处在阻塞等待状态, pthread_cond_signal() 也会成功返回(若唤醒线程时线程
都处于工作状态,则在某线程当前任务执行结束后,线程从相应的队列中获取数据任务继续
执行处理)

(2)激活所有等待线程 

 int pthread_cond_broadcast(pthread_cond_t *cond);
  • 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY

总览:

3、条件变量原则

(1)等待条件变量总是返回被锁住的互斥量
(2)条件变量的作用是发信号,而不是互斥
(3)一个条件变量应该只与一个“状态描述(也有称呼为“谓词”,所谓谓词,即:描述代码所需不变量的状态的语句)”相关联。
(4)所有并发地(同时)去等待一个条件变量的线程必须指定同一个“互斥量”。
如:不允许线程1使用互斥量A等待条件变量A,而线程2使用互斥量B等待条件变量A。但是:线程1使用互斥量A等待条件变量A,线程2使用互斥量A等待条件变量B是合理的。即任何条件变量在特定的时刻只能够与一个互斥量相互关联,而互斥量则可以同时与多个条件变量关联。
(5)条件变量提供了“信号单播(注意:这里的所谓信号并非Linux下的SIGxxx)”和“信号广播”两种方式,基于更安全、高效等因素,优先考虑使用“广播信号”方式。

(6)线程发信号或广播条件变量时候看到的内存数据,同样也可以被唤醒的其他线程看到。而在发信号或广播之后写入内存的数据不会被唤醒的线程看到,即使写操作发生在线程被唤醒之前。

(7)一个内存地址一次只能保持一个值;不要让线程竞争以优先获得访问权。

(8)在等待线程醒来时候,检查其“状态”是否为真是个不错的主意;同时应该总是在一个循环中等待条件变量。
(9)条件变量是程序用来等待某个“状态”为真的机制。

4.示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>//生产者消费者模型pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//互斥锁
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;//条件变量struct msg{struct msg* next;int num;
};
struct msg *head;void *customer1(void *p)//消费者
{//假如消费者先执行struct msg *mp;for( ; ; ){pthread_mutex_lock(&lock);while(head == NULL)//没有食物,则阻塞等待pthread_cond_wait(&has_product,&lock);//删除这个节点(消费掉食物)mp = head;head = mp->next;pthread_mutex_unlock(&lock);//消费掉printf("customer1 %d\n",mp->num);free(mp);sleep(1);}
}
void *customer2(void *p)//消费者
{//假如消费者先执行struct msg *mp;for( ; ; ){pthread_mutex_lock(&lock);while(head == NULL)//没有食物,则阻塞等待pthread_cond_wait(&has_product,&lock);//删除这个节点(消费掉食物)mp = head;head = mp->next;pthread_mutex_unlock(&lock);//消费掉printf("customer2 %d\n",mp->num);free(mp);sleep(1);}
}
void *product(void *p)//生产者
{struct msg *mp;for( ; ; ){mp = (struct msg *)malloc(sizeof(struct msg));//模拟一张饼mp->num = rand()%1000+1;printf("product %d\n",mp->num);//向链表加入节点pthread_mutex_lock(&lock);//头插法mp->next = head;head = mp;//唤醒阻塞在条件上的进程pthread_cond_signal(&has_product);pthread_mutex_unlock(&lock);sleep(1);}}int main()
{pthread_t pid1,pid2 ,cid;pthread_create(&pid2, NULL, &customer2, NULL);pthread_create(&pid1, NULL, &customer1, NULL);pthread_create(&cid, NULL, &product, NULL);pthread_join(pid1, NULL);pthread_join(pid2, NULL);pthread_join(cid, NULL);return 0;
}

运行结果:

【3】信号量

信号量是用来解决线程间同步或互斥的一种机制,也是一个特殊的变量,变量的值代表着当前可以利用的资源。

  如果等于0,那就意味着现在没有资源可用。

  根据信号量的值可以将信号量分为二值信号量和计数信号量:

  (计数信号量)就像一间公共厕所,里面一共有十个坑(最大是32767),算是十个资源。在同一时间可以容纳十个人,当满员的时候,外面的人必须等待里面的人出来,释放一个资源,然后才能在进一个,当他进去之后,厕所又满员了,外面的人还得继续等待……

  (二值信号量)就像自己家的卫生间,一般只有一个马桶,在同一时间只能有一个人来用。

  信号量只能进程两个原子操作,P操作和V操作,

概念:

  原子操作,就是不能被更高等级中断抢夺优先的操作。

  由于操作系统大部分时间处于开中断状态,所以,一个程序在执行的时候可能被优先级更高的线程中断。

  而有些操作是不能被中断的,不然会出现无法还原的后果,这时候,这些操作就需要原子操作。就是不能被中断的操作。

  P操作:如果有可用的资源(信号量>0),那么占用一个资源(信号量-1)。如果没有可用的资源(信号量=0),则进程被阻塞,直到系统重新给他分配资源。

  V操作:如果在该信号量的等待队列中有进程在等待该资源,则唤醒一个进程,否则释放一个资源(信号量+1)

PV原子操作主要用于进程或线程间的同步和互斥这两种典型情况。若用于互斥,几个进程(或线程)往往只设置一个信号量sem,其操作流程如图1所示。当信号量用于同步操作时,往往会设置多个信号量,并安排不同的初始值来实现它们之间的顺序执行,其操作流程如图2所示。

POSIX提供两种信号量,有名信号量无名信号量,有名信号量一般是用在进程间同步,无名信号量一般用在线程间同步。

总览:

1.无名信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);确定信号量的初始值参数:sem: 信号量 pshared:    0: 用于线程间同步                   1: 用于进程间同步value:N值。(指定同时访问的线程数)sem_destroy();sem_wait();        一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 (对比 pthread_mutex_lock)sem_post();        一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock)
 #include <semaphore.h>int sem_wait(sem_t *sem);sem_trywait()与sem_wait()相同,不同之处在于,如果无法立即执行减量,则调用将返回错误(错误号设置为EAGAIN)而不是阻塞。int sem_trywait(sem_t *sem);int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);sem_timedwait()与sem_wait()相同,不同之处在于abs_timeout指定了在无法立即执行减量操作时调用应阻塞的时间限制。 abs_timeout参数指向一个结构此结构定义如下:struct timespec {time_t tv_sec; / *秒* /长tv_nsec; / *纳秒[0 .. 999999999] * /};如果超时已在调用时到期,并且信号无法立即锁定,则sem_timedwait()会失败,并显示超时错误(将errno设置为ETIMEDOUT)。如果可以立即执行该操作,则无论abs_timeout的值如何,sem_timedwait()都不会因超时错误而失败。此外,在这种情况下不检查abs_timeout的有效性。以上所有返回值:成功:0  ,失败:-1
 #include <semaphore.h>int sem_post(sem_t *sem);
 #include <semaphore.h>int sem_destroy(sem_t *sem);销毁sem指向的地址上的未命名信号量。销毁当前已阻塞其他进程或线程的信号量会产生未定义的行为。使用已销毁的信号量会产生不确定的结果返回值成功返回0; 如果出现错误,则返回-1,并且将errno设置为指示错误。

案例:信号量就是保护临界资源的访问。凡是临界资源的,我们都可以用信号量操作, 多个生产者,多个消费者使用信号量最方便。

我们此时可以回忆一下之前说过的互斥锁和条件变量。互斥锁 类似于二值信号量。

而条件变量 和信号量、互斥锁有着很大的不同,单单说就是事件通知机制。

   /*信号量实现 生产者 消费者问题*/  #include <stdlib.h>  #include <unistd.h>  #include <pthread.h>  #include <stdio.h>  #include <semaphore.h>  #define NUM 5                 int queue[NUM];                                     //全局数组实现环形队列  sem_t blank_number, product_number;                 //空格子信号量, 产品信号量  void *producer(void *arg)  {  int i = 0;  while (1) {  sem_wait(&blank_number);                    //生产者将空格子数--,为0则阻塞等待  queue[i] = rand() % 1000 + 1;               //生产一个产品  printf("----Produce---%d\n", queue[i]);          sem_post(&product_number);                  //将产品数++  i = (i+1) % NUM;                            //借助下标实现环形  sleep(rand()%1);  }  }  void *consumer(void *arg)  {  int i = 0;  while (1) {  sem_wait(&product_number);                  //消费者将产品数--,为0则阻塞等待  printf("-Consume---%d\n", queue[i]);  queue[i] = 0;                               //消费一个产品   sem_post(&blank_number);                    //消费掉以后,将空格子数++  i = (i+1) % NUM;  sleep(rand()%3);  }  }  int main(int argc, char *argv[])  {  pthread_t pid, cid;  sem_init(&blank_number, 0, NUM);                //初始化空格子信号量为5, 线程间共享   sem_init(&product_number, 0, 0);                //产品数为0  pthread_create(&pid, NULL, producer, NULL);  pthread_create(&cid, NULL, consumer, NULL);  pthread_join(pid, NULL);  pthread_join(cid, NULL);  sem_destroy(&blank_number);  sem_destroy(&product_number);  return 0;  }

2.有名信号量

  •   创建有名信号量:

  创建或者打开一个信号量,需要使用sem_open()函数,函数原形如下:  

sem_t sem_open(const char * name, int oflag, mode_t mode, unsigned int value)返回值sem_t 是一个结构,如果函数调用成功,则返回指向这个结构的指针,里面装着当前信号量的资源数。参数name,就是信号量的名字,两个不同的进程通过同一个名字来进行信号量的传递。参数oflag,当他是O_CREAT时,如果name给出的信号量不存在,那么创建,此时必须给出mode和vaule。当他是O_EXCL时,好像没有啥太重要的意义。参数mode,很好理解,用来指定信号量的权限。参数vaule,则是信号量的初始值。
  •   关闭有名信号量: sem_close(sem_t *sem)

  关闭有名信号量所使用的函数是sem_close(sem_t *sem)

  这个函数只有一个参数,意义也非常明显,就是指信号量的名字。

案例:进程间

#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>
#include<errno.h>
#include<sys/stat.h>
#include<fcntl.h>#define SEM_NAME "name"int main()
{sem_t *sem_test;sem_test = sem_open("ni", O_CREAT, 0644, 0);if(sem_test < 0){printf("A进程创建信号量失败!errno=%d\n",errno);exit(-1);}printf("进程A进入等待……\n");printf("A\n");sem_wait(sem_test);printf("C\n");sem_post(sem_test);printf("A进程执行完毕!\n");sem_close(sem_test);       sem_unlink("ni");return 0;}
#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>
#include<errno.h>
#include<sys/stat.h>
#include<fcntl.h>#define SEM_NAME "name"int main()
{sem_t *sem_test;sem_test = sem_open("ni",0);if(sem_test < 0){printf("B进程创建信号量失败!errno=%d\n",errno);exit(-1);}printf("B\n");sem_post(sem_test);printf("B进程执行完毕!\n");sem_close(sem_test);sem_unlink("ni");return 0;
}

1. 互斥锁必须是谁上锁就由谁来解锁,而信号量的wait和post操作不必由同一个线程执行。
2. 互斥锁要么被锁住,要么被解开,和二值信号量类似
3. sem_post是各种同步技巧中,唯一一个能在信号处理程序中安全调用的函数
4. 互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性 
5. 互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。
6. 信号量有计数值,每次信号量post操作都会被记录,而条件变量在发送信号时,如果没有线程在等待该条件变量,那么信号将丢失。

条件变量和信号量的区别:

(1)使用条件变量可以一次唤醒所有等待者,而这个信号量没有的功能,感觉是最大区别。

(2)信号量是有一个值(状态的),而条件变量是没有的,没有地方记录唤醒(发送信号)过多少次,也没有地方记录唤醒线程(wait返回)过多少次。从实现上来说一个信号量可以是用mutex + counter + condition variable实现的。因为信号量有一个状态,如果想精准的同步,那么信号量可能会有特殊的地方。信号量可以解决条件变量中存在的唤醒丢失问题。

(3)在Posix.1基本原理一文声称,有了互斥锁和条件变量还提供信号量的原因是:“本标准提供信号量的而主要目的是提供一种进程间同步的方式;这些进程可能共享也可能不共享内存区。互斥锁和条件变量是作为线程间的同步机制说明的;这些线程总是共享(某个)内存区。这两者都是已广泛使用了多年的同步方式。每组原语都特别适合于特定的问题”。尽管信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。应当根据实际的情况进行决定。信号量最有用的场景是用以指明可用资源的数量

linux下多线程编程、线程间同步通信及应用详解、及踩过的坑相关推荐

  1. Linux多线程编程---线程间同步(互斥锁、条件变量、信号量和读写锁)

    本篇博文转自http://zhangxiaoya.github.io/2015/05/15/multi-thread-of-c-program-language-on-linux/ Linux下提供了 ...

  2. linux 多线程 semaphore ,Linux下多线程编程-Pthread和Semaphore使用.doc

    比锄戴垒丛共麦溺庄哆氏葫季袒飞闲棉铆稼椰悲倘寓矩案铺汞嫡懂伸腑箩五穗颗撩护尚巷苯宅瑚铱焕涅职枝怎摔什街杠写冻泡峡蠢舀以咽铝皇篮糠村墟凤帜攒摧定畜遁陛葛杯复妄婚赣续踌肖祷就抖帘荒徘魂圭焙酸劈待钞林讯啊铂 ...

  3. [原创]手把手教你Linux下的多线程设计--Linux下多线程编程详解(一)

    本文可任意转载,但必须注明作者和出处. [原创]手把手教你Linux下的多线程设计(一)                                       --Linux下多线程编程详解 原 ...

  4. wxpython多线程 假死_wxpython多线程防假死与线程间传递消息实例详解

    wxpython中启用线程的方法,将GUI和功能的执行分开. 网上关于python多线程防假死与线程传递消息是几年前的,这里由于wxpython和threading模块已经更新最新,因此给出最新修改代 ...

  5. 操作系统之多线程编程—读者优先/写者优先详解

    操作系统之进程调度--优先权法和轮转法(附上样例讲解) 操作系统之银行家算法-详解流程及案例数据 操作系统之多线程编程-读者优先/写者优先详解 操作系统之存储管理--FIFO算法和LRU算法 操作系统 ...

  6. 创建三个并发进程linux,Linux下几种并发服务器的实现模式(详解)

    1>单线程或者单进程 相当于短链接,当accept之后,就开始数据的接收和数据的发送,不接受新的连接,即一个server,一个client 不存在并发. 2>循环服务器和并发服务器 1.循 ...

  7. 1 linux下tcp并发服务器的几种设计的模式套路,Linux下几种并发服务器的实现模式(详解)...

    1>单线程或者单进程 相当于短链接,当accept之后,就开始数据的接收和数据的发送,不接受新的连接,即一个server,一个client 不存在并发. 2>循环服务器和并发服务器 1.循 ...

  8. 深入浅出多线程编程实战(五)ThreadLocal详解(介绍、使用、原理、应用场景)

    深入浅出多线程编程实战(五)ThreadLocal详解(介绍.使用.原理.应用场景) 文章目录 一.ThreadLocal简介 二.ThreadLocal与Synchronized区别 三.Threa ...

  9. dns日志级别 linux,linux下DNS服务器视图view及日志系统详解

    linux下DNS服务器视图view及日志系统详解DNS服务器ACL:在named.conf文件中定义ACL功能如同bash当中定义变量,便于后续引用 ACL格式: acl ACL名称 { IP地址1 ...

  10. linux在vi创建文件,Linux下创建文本文件(vi/vim命令使用详解)

    vi test.txt 或者 vim test.txt 再或者 touch test.txt vim是vi的升级版,指令更多,功能更强. 下面是收集的vim用法,当在vim里面要实现退出,首先要做的是 ...

最新文章

  1. 自动驾驶技术之——虚拟场景数据库研究
  2. 系统目录结构文件类型及ls.alias命令
  3. RabbitMq 本地连接报错 org.springframework.amqp.AmqpIOException: java.io.IOException
  4. 1.7 Python基础知识 - 模块初识
  5. python echo服务器_python常用框架 echo server 的测试
  6. 微型计算机按原理可分为那几种,东师微型机原理与应用19秋在线作业2题目【标准答案】...
  7. RandomAccessFile 随机存取文件任意位置数据
  8. JS的一些时间获取和计算公用方法封装
  9. 智能优化算法:堆优化算法-附代码
  10. 关于如何使用IfcRelAggregates来对IFC中的元素进行关联
  11. 【数学建模】论文模板和latex模板
  12. GRADS软件初步学习
  13. 欧姆龙nb触摸屏通信_欧姆龙触摸屏 NB系列
  14. 移动应用支付宝开发创建应用_2020年可与十大移动应用开发公司合作
  15. SpringBoot使用爬虫(初级阶段)
  16. 周末作业-循环练习题(2)
  17. cst matlab,CST-MATLAB-API-1.0.0 Linking matlab and cst - 下载 - 搜珍网
  18. 解决锐捷客户端出现密码不匹配,请输入正确密码问题
  19. 解决anaconda下载pytorch慢问题(清华镜像源)
  20. sync.Pool 问题argument should be pointer-like to avoid allocations (SA6002)

热门文章

  1. 【计算机组成原理】寄存器的本质——锁存器
  2. Spring 动态代理
  3. 联想笔记本连不上手机热点_联想笔记本连不上无线_联想笔记本连不上热点
  4. 三菱PLC控制步进电机
  5. 13、ARM嵌入式系统:通过旋钮控制蜂鸣器声音大小
  6. Android平台语音交友软件源码开发,语音通话的实现
  7. 计算机专业论文答辩ppt,计算机毕论文答辩PPT(完整版).ppt
  8. 联想lenovo G40-70M 无线网卡白名单跳过
  9. python爬虫爬取机床展名录
  10. VMware安装windows server 2008 R2