linux中pthread_join()与pthread_detach()详解_魏波-的博客-CSDN博客_pthread_detach
pthread_join()和pthread_detach()二者的区别_modi000的博客-CSDN博客
NPTL
察看当前pthread库版本
getconf GNU_LIBPTHREAD_VERSION
NPTL实现机制(POSIX),Native POSIX Thread Library
使用线程库时gcc指定
–lpthread
- 查看线程函数列表
man -k pthread
,不存在则安装线程man page:sudo apt-get install manpages-posix-dev
线程概念
进程的概念体现出两个特点:资源(代码和数据空间、打开的文件等)以及调度/执行。
线程是进程内的独立执行代码的实体和调度单元
一个进程内的所有线程共享进程的很多资源(这种共享又带来了同步问题)
什么是线程
LWP
:light weight process
轻量级的进程,本质仍是进程(在Linux环境下)
进程:独立地址空间,拥有PCB
线程:也有PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。 独居(进程);合租(线程)。
Linux下:
- 线程:最小的执行单位
- 进程:最小分配资源单位,可看成是只有一个线程的进程。
Linux线程实现原理
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。
轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
进程可以蜕变成线程
线程可看做寄存器和栈的集合
在linux下,线程最是小的执行单位;进程是最小的分配资源单位
察看LWP
号:ps –Lf pid
查看指定进程里面的所有lwp
号(和线程ID不同)
三级映射:进程PCB —> 页目录(可看成数组,首地址位于PCB中) —> 页表 —> 物理页面 —> 内存单元
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork
,还是创建线程的pthread_create
,底层实现都是调用同一个内核函数clone
。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_*
是库函数,而非系统调用。
线程间共享
线程间共享
- 进程指令
- 内存地址空间(.text/.data/.bss/heap/共享库)
- 文件描述符表
- 信号处理程序
- 当前工作目录
- 用户ID和组ID
线程私有
- 线程ID
- 寄存器集合(包括PC和栈指针)
- 栈(用于存放局部变量)
errno
变量- 信号掩码
- 调度优先级
线程优缺点
优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
线程与进程对比
- 线程只拥有少量在运行中必不可少的资源
- PC指针:标识当前线程执行的位置
- 寄存器:当前线程执行的上下文环境
- 栈:用于实现函数调用、局部变量(局部变量是私有的)
- 进程占用资源多,线程占用资源少,使用灵活
- 线程不能脱离进程而存在,线程的层次关系,执行顺序并不明显,会增加程序的复杂度
- 没有通过代码显示创建线程的进程,可以看成是只有一个线程的进程
控制原语对比
控制操作 | 进程操作API | 线程操作API |
---|---|---|
创建 | fork ,vfork |
pthread_create |
终止 | exit |
pthread_exit |
等待 | wait 、waitpid |
pthread_join |
读取ID | getpid |
pthread_self |
杀死 | kill |
pthread_cancel |
线程ID
同进程一样,每个线程也有一个线程ID
进程ID在整个系统中是唯一的,线程ID只在它所属的进程环境中唯一
线程ID的类型是pthread_t
,在Linux中的定义:/usr/include/bits/pthreadtypes.h
(实际位置可能有变化)
typedef unsigned long int pthread_t
线程ID是用来区分线程的,线程号是用来分配资源。
线程启动
线程创建后等待系统调度,被调度后从线程启动例程函数
一次性创建多个线程,调度顺序与创建顺序无关
线程终止
线程的三种终止方式
- 线程从启动例程函数中返回,函数返回值作为线程的退出码
- 线程被同一进程中的其他线程取消
- 线程在任意函数中调用
pthread_exit
函数终止执行
线程控制原语
获取线程ID:pthread_self()
创建线程:pthread_create()
线程退出:pthread_exit()
等待线程并回收:pthread_join()
终止线程:pthread_cancel()
分离线程:pthread_detach()
判断两个线程ID是否相等:int pthread_equal(pthread_t t1, pthread_t t2);
(目前没啥用,可以直接t1 == t2
)
pthread_self函数
man 3 pthread_self
pthread_self
函数可以让调用线程获取自己的线程ID
1 |
|
返回值:调用线程的线程ID
线程ID:`pthread_t
类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
注意:不应使用全局变量 pthread_t tid
,在子线程中通过pthread_create
传出参数来获取线程ID,而应使用pthread_self
。
pthread_create函数
man 3 pthread_create
pthread_create
函数用于创建一个线程
1 |
|
调用pthread_create
函数的线程是所创建线程的父线程
参数:
thread
:指向线程ID的指针,当函数成功返回时将存储所创建的子线程IDattr
:用于指定线程属性(一般直接传入空指针NULL,采用默认线程属性)start_rtn
:线程的启动例程函数(类似进程的main函数入口)指针,创建的线程首先执行该函数代码(可以调用其他函数)arg
:向线程的启动例程函数传递信息的参数
返回值:成功返回0,出错时返回各种错误码(Linux环境下,所有线程特点,失败均直接返回错误号。)
在一个线程中调用pthread_create()
创建新的线程后,当前线程从pthread_create()
返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create
的函数指针start_routine
决定。start_routine
函数接收一个参数,是通过pthread_create
的arg
参数传递给它的,该参数的类型为void *
,这个指针按什么类型解释由调用者自己定义。start_routine
的返回值类型也是void *
,这个指针的含义同样由调用者自己定义。start_routine
返回时,这个线程就退出了,其它线程可以调用pthread_join
得到start_routine
的返回值,类似于父进程调用wait(2)
得到子进程的退出状态。
pthread_create
成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)
可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t
,它只在当前进程中保证是唯一的,在不同的系统中thread_t
这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)
可以获得当前线程的id。
attr
参数表示线程属性,一般传NULL给attr参数,表示线程属性取缺省值。深入了解参考APUE。
代码示例
创建一个子线程
用法和进程创建子进程类似。
不同:进程结束(main函数return或exit),子线程不会继续执行,所以要等子线程执行完。
1 |
|
创建N个子线程
创建5个线程,每个线程打印自己的序号
1 |
|
注意代码中两种给子线程传参的方式:
1)传值:子线程按顺序输出
会有警告warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
。
因为将int
强制转换为void*
,再将void*
强制转换为int
32位系统下指针大小为4字节,64位是8字节。int
大小为4字节。int
强制转换为void*
,高位补0。void*
再转回int
,取低位。
2)传地址:出问题
现象:第一次5个子线程都没有输出。第二次注释子进程中的睡眠,5个子线程输出6th
,不合预期
原因:主线程循环5次创建5个子线程,i变为5,执行sleep
,让出cpu。传地址后,子线程访问的i
就不是预期的0、1、2、3、4
,而是5
,各自加1输出了6th
。子线程不去掉sleep
时,睡眠时间比主线程长,主线程结束后,整个进程结束,子线程没来得及输出。
线程共享全局变量
1 |
|
pthread_exit函数
man 3 pthread_exit
作用:退出当前线程
1 |
|
retval
该指针将参数传递给pthread_join
函数(与exit
函数参数用法类似)
示例
1 |
|
注意
多线程环境中,应尽量少用,或者不使用exit
函数,取而代之使用pthread_exit
函数,将单个线程退出。任何线程里exit
导致进程退出,其他线程未工作结束,主控线程退出时不能return
或exit
。
另注意,pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc
分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
return
:返回到调用者那里去pthread_exit()
:将调用该函数的线程退出exit
: 将进程退出
phread_join函数
man 3 pthread_join
阻塞等待线程退出,获取线程退出状态。(对应进程中 waitpid() 函数)
1 |
|
调用该函数的父线程将一直被阻塞,直到指定的子线程终止
返回值:成功返回0,否则返回错误编号
参数:
thread
:需要等待的子线程IDretval
:线程返回值- 若线程从启动例程返回
return
,retval
将包含返回码 - 若线程被取消
pthread_cancel
,retval
指向的内存单元值置为PTHREAD_CANCELED
- 若线程通过调用
pthread_exit
函数终止,retval
就是调用pthread_exit
时传入的参数 - 若不关心线程返回值,可直接将该参数设置为空指针
NULL
- 若线程从启动例程返回
结构体示例
1 |
|
回收N个线程
1 |
|
pthread_cancel函数
man 3 pthread_cancel
线程调用该函数可以取消同一进程中的其他线程(即让该线程终止)
1 |
|
参数:tid
需要取消的线程ID
返回值:成功返回0,出错返回错误编号
注意事项
- 在默认情况下,
pthread_cancel
函数与被取消线程(ID等于tid的线程)自身调用pthread_exit
函数(参数为PTHREAD_CANCELED
)效果等同 - 线程可以选择忽略取消方式或者控制取消方式
pthread_cancel
并不等待线程终止,它仅仅是提出请求
【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write
等,执行命令man 7 pthreads
可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。
如线程中没有取消点,可以通过调用pthread_testcancel
函数自行设置一个取消点。
被取消的线程,退出值定义在Linux的pthread
库中。常数PTHREAD_CANCELED
的值是-1
。可在头文件pthread.h
中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)
。因此当我们对一个已经被取消的线程使用pthread_join
回收时,得到的返回值为-1。
取消点示例
1 |
|
运行结果:
将tfn2
中的while
中的语句注释掉,即第2个子线程一直运行但不涉及系统调用。第2 个子线程不会终止,主线程会一直阻塞等待
pthread_detach函数
man 3 pthread_detach
作用:实现线程分离
1 |
|
返回值:成功:0;失败:错误号
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。
也可使用 pthread_create
函数参2(线程属性)来设置线程分离。
特点:
- 使用
pthread_detach
函数后,使线程处于分离态; - 使用
pthread_detach
函数后,线程在退出后,会自己清理资源 - 相较
pthread_join
,使用pthread_detach
函数不会阻塞主线程,但是无法获取线程的返回值。 - pthread_detach使用时,依然需要配合sleep函数或者while(1);,否则无法保证子线程先于主线程执行完,且不能再pthread_join回收该线程
示例:线程分离后,不能使用pthread_join
回收
1 |
|
线程属性
linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
pthead_attr_t结构体
1 | typedef struct { |
主要结构体成员:
线程分离状态
线程栈大小(默认平均分配)
线程栈警戒缓冲区大小(位于栈末尾) 参 APUE.12.3 线程属性
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init
,这个函数必须在pthread_create
函数之前调用。之后须用pthread_attr_destroy
函数来释放资源。
线程属性主要包括如下属性:
- 作用域(scope)
- 栈尺寸(stack size)
- 栈地址(stack address)
- 优先级(priority)
- 分离的状态(detached state)
- 调度策略和参数(scheduling policy and parameters)
默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
相关操作
1 | // 线程属性初始化 |
线程属性初始化
注意:应先初始化线程属性,再pthread_create
创建线程
初始化线程属性:
int pthread_attr_init(pthread_attr_t *attr);
成功:0;失败:错误号
销毁线程属性所占用的资源:
int pthread_attr_destroy(pthread_attr_t *attr);
成功:0;失败:错误号
线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
线程分离状态的函数:
设置线程属性,分离or非分离:
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
获取程属性,分离or非分离:
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
成功:0;失败:错误号
参数:
attr
:已初始化的线程属性detachstate
PTHREAD_CREATE_DETACHED
(分离线程)PTHREAD_CREATE_JOINABLE
(非分离线程)
注意:如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create
函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create
的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程的启动历程函数里调用pthread_cond_timedwait
函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create
返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()
之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
线程的栈地址
POSIX.1定义了两个常量 _POSIX_THREAD_ATTR_STACKADDR
和_POSIX_THREAD_ATTR_STACKSIZE
检测系统是否支持栈属性。也可以给sysconf
函数传递_SC_THREAD_ATTR_STACKADDR
或 _SC_THREAD_ATTR_STACKSIZE
来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack
和pthread_attr_getstack
两个函数分别设置和获取线程的栈地址。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
成功:0;失败:错误号
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
成功:0;失败:错误号
参数:
attr
:指向一个线程属性的指针stackaddr
:返回获取的栈地址stacksize
:返回获取的栈大小
线程的栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
函数pthread_attr_getstacksize
和 pthread_attr_setstacksize
提供设置。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
成功:0;失败:错误号
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
成功:0;失败:错误号
参数:
attr
:指向一个线程属性的指针stacksize
:返回线程的堆栈大小
代码示例
分离线程示例
1 |
|
线程数量示例
1 | // max_pthread.c |
线程栈示例
1 |
|
线程使用注意事项
主线程退出其他线程不退出,主线程应调用pthread_exit
避免僵尸线程
pthread_join
— 回收子线程pthread_detach
— 分离线程pthread_create
— 创建时设置分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
malloc
和mmap
申请的内存可以被其他线程释放应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
同步
所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等
而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。
线程同步
同步即协同步调,按预定的先后次序运行。
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
举例1:银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000
举例2:内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续 从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。
产生的现象叫做与时间有关的错误(time related)。为了避免这种数据混乱,线程需要同步。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
数据混乱原因
资源共享(独享资源则不会)
调度随机(意味着数据访问会出现竞争)
线程间缺乏必要的同步机制。
以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
任务关系
Linux系统中多任务(进程/线程)之间的关系
- 独立:仅竞争CPU资源
- 互斥:竞争除CPU外的其他资源
- 同步:协调彼此运行的步调,保证协同运行的各个任务具有正确的执行次序
- 通信:数据共享,彼此间传递数据或信息,以协同完成某项工作
线程数据共享
任务互斥问题
任务互斥—资源共享关系(间接相互制约关系)
- 任务本身之间不存在直接联系。一个任务正在使用某个系统资源,另外一个想用该资源的任务就必须等待,而不能同时使用
全局变量存储在进程数据段中,被线程所共享。线程对全局变量的访问,要经历三个步骤
将内存单元中的数据读入寄存器
对寄存器中的值进行运算
- 将寄存器中的值写回内存单元
解决方法:互斥量
任务同步问题
任务同步—相互合作关系(直接相互制约关系)
- 两个或多个任务为了合作完成同一个工作,在执行速度或某个确定的时序点上必须相互协调,即一个任务的执行必须依赖另一个任务的执行情况
程序设计中存在这样的情况:多个线程都要访问临界资源又要相互合作(线程间同时存在互斥关系和同步关系)
线程A先执行某操作(例如对全局变量x的修改)后,线程B才能(根据变量x的值判断)执行另一操作
(可能是对全局变量x的修改),该如何实现?
- Linux提供了条件变量机制:条件变量与互斥量一起使用时,允许线程以互斥的方式阻塞等待特定条件的发生(同步)
临界资源:在一段时间内只允许一个任务(线程或进程)访问的资源。诸任务间应采取互斥方式,实现对资源的共享
- 共享变量,打印机等属于临界资源
- 访问临界资源的那段代码被称为临界区
线程同步
互斥量mutex
互斥量确保同一时间里只有一个线程访问临界资源或进入临界区
互斥量(mutex)本质上是一把锁
- 在访问临界资源前,对互斥量进行加锁
- 在访问完成后对互斥量解锁
- 对互斥量加锁后,任何其他试图对互斥量加锁的线程将会被阻塞,直到互斥量被解锁为止
当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是操作系统提供的一把建议锁(又称协同锁),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
互斥量的操作
头文件:#include <pthread.h>
- 定义互斥量变量:
pthread_mutex_t mutex;
- 初始化互斥量变量:
pthread_mutex_init
函数 - 对互斥量加锁:
pthread_mutex_lock
函数(阻塞)pthread_mutex_trylock
函数(非阻塞)pthread_mutex_timedlock
函数(限时加锁)
- 对互斥量解锁:
pthread_mutex_unlock
函数 - 销毁互斥量变量:
pthread_mutex_destroy
函数
以上5个函数的返回值都是:成功返回0,出错返回错误码。
pthread_mutex_t
类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。只有两种取值1、0。
函数原型
1)pthread_mutex_init
作用:初始化互斥量变量
静态初始化
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
- 如果互斥锁 mutex 是静态分配的(定义在全局,或加了
static
关键字修饰),可以直接使用宏进行初始化
动态初始化
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
mutex
:指向互斥量的指针attr
:设置互斥量的属性,通常可采用默认属性(线程共享),传入空指针(NULL
)。
restrict
关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
2)pthread_mutex_destroy
作用:销毁互斥量
互斥量在使用完毕后,必须要对互斥量进行销毁,以释放资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex
:即互斥量
3)pthread_mutex_lock
在对临界资源访问之前,需要对互斥量进行加锁操作
int pthread_mutex_lock(pthread_mutex_t *mutex);
当调用pthread_mutex_lock
时,若互斥量已被加锁,则调用线程将被阻塞直到可以完成加锁操作为止。
4)pthread_mutex_trylock
尝试加锁,非阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 调用该函数时,若互斥量未加锁,则对该互斥量加锁,返回0;
- 若互斥量已加锁,则函数直接返回错误码
EBUSY
(不会阻塞调用线程)
5)pthread_mutex_unlock
互斥量的解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
6)pthread_mutex_timedlock
限时加锁
1 | int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, |
man sem_timedwait
可以查看struct timespec
结构体
形参abstime
:绝对时间。
如:time(NULL)
返回的就是绝对时间。而alarm(1)
是相对时间,相对当前时间定时1秒钟。
错误用法:
1 | struct timespec t = {1, 0}; |
正确用法:
1 | time_t cur = time(NULL); // 获取当前时间 |
代码示例
两个线程共享stdout
资源,分别打印hello world
和HELLO WORLD
1 | include <stdio.h> |
小结:在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。
死锁
死锁场景
1. 线程试图对同一个互斥量A加锁两次。
1. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
1. 震荡(哲学家进餐问题,同时拿起筷子,同时释放筷子,同时再拿起筷子...)
避免方法:
- 保证资源的获取顺序,要求每个线程获取资源的顺序一致
- 当得不到所有所需资源时,放弃已经获得的资源(不同时),等待
读写锁rwlock
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
读写锁引入
问题描述:
- 在对临界资源的访问中,更多的是读操作,而写操作较少,只有互斥量机制可能会影响访问效率
- 期望对临界资源的访问控制粒度更精细,任一时刻允许多个线程对临界资源进行读操作,但只允许一个线程对临界资源进行写操作
互斥关系:
- 读操作-写操作互斥
- 写操作-写操作互斥
- 读操作-读操作不互斥
同步关系:
- 缓冲区不满,才允许写操作
- 缓冲区不空,才允许读操作
读写锁通信机制
在保证互斥的基础上,Linux提供了对临界资源访问控制粒度更细的读写锁机制
读写锁是写模式加锁(写锁)时, 解锁前,所有对该锁加锁的线程都会被阻塞。
读写锁是读模式加锁(读锁)时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
读写锁是读模式加锁时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
读写锁的操作
读写锁的操作与互斥量的操作非常类似
头文件:#include <pthread.h>
- 定义读写锁变量:
pthread_rwlock_t rwlock;
- 初始化读写锁变量:
pthread_rwlock_init
函数 - 访问临界资源(读操作或写操作)前对读写锁加锁
- 阻塞申请读锁:
pthread_rwlock_rdlock
函数 - 阻塞申请写锁:
pthread_rwlock_wrlock
函数 - 非阻塞申请读锁:
pthread_rwlock_tryrdlock
函数 - 非阻塞申请写锁:
pthread_rwlock_trywrlock
函数 - 限时申请读锁:
pthread_rwlock_timerdlock
函数 - 限时申请写锁:
pthread_rwlock_timewrlock
函数
- 阻塞申请读锁:
- 解锁(包括读锁和写锁):
pthread_rwlock_unlock
函数 - 销毁读写锁变量:
pthread_rwlock_destroy
函数
以上函数成功返回0,失败返回错误码
函数原型
1)读写锁初始化
静态初始化:pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
动态初始化:
1 | int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, |
attr
表读写锁属性,通常使用默认属性,传NULL
即可。
2)销毁读写锁变量
1 | int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); |
3)请求读锁
1 | int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 阻塞 |
4)请求写锁
1 | int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 阻塞 |
5)限时加锁
1 | int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime); // 限时加读锁 |
abstime
的使用参考前面互斥量的pthread_mutex_timedlock
函数
6)读写锁解锁
1 | int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); |
- 就近原则。加锁(读锁/写锁)与解锁配对出现,为代码中距离最近的加锁操作解锁
代码示例
1 | /* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */ |
条件变量cond
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
条件变量的操作
头文件:#include <pthread.h>
- 定义互斥量变量,定义条件变量
pthread_cond_t cond;
- 初始化互斥量,初始化条件变量
pthread_cond_init
函数 - 等待条件线程x(假设先执行)
- 互斥量加锁 —> XX操作 —>
- 等待条件变量:
pthread_cond_wait
函数、pthread_cond_timedwait
函数 - —> XX操作 —> 互斥量解锁
- 触发条件线程y(假设后执行)
- 触发条件变量:
pthread_cond_signal
函数、pthread_cond_broadcast
函数
- 触发条件变量:
- 销毁互斥量变量,销毁条件变量
pthread_cond_destroy
函数的返回值都是:成功返回0, 失败返回错误码。
为什么条件变量需要和互斥量配合使用
- 条件变量的使用场景伴随共享资源的使用,例如全局变量
- 在调用
pthread_cond_wait
前,需要使互斥量处于加锁状态,这样可以通过原子操作的方式,将调用线程放到该条件变量等待线程队列(临界资源)中
调用pthread_cond_wait
函数后内核自动执行的操作:
- 在线程阻塞等待条件变量之前,调用
pthread_mutex_unlock
- 若条件变量被其他线程触发,在该线程被唤醒后,调用
pthread_mutex_lock
函数说明
1)条件变量初始化
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化:
1 | int pthread_cond_init(pthread_cond_t *restrict cond, |
cond
:条件变量attr
:条件变量属性,若为NULL
,则使用默认属性
2)条件变量销毁
int pthread_cond_destroy(pthread_cond_t * cond);
3)等待条件变量
1 | // 将使调用线程进入阻塞状态,直到条件被触发 |
pthread_cond_wait
函数作用:( 1.2.两步为一个原子操作)
阻塞等待条件变量cond(参1)满足
释放已掌握的互斥锁(解锁互斥量)相当于
pthread_mutex_unlock(&mutex);
当被唤醒,
pthread_cond_wait
函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
1 | // 限时等待一个条件变量 |
abstime
的使用参考前面互斥量的pthread_mutex_timedlock
函数
4)触发条件变量
1 | int pthread_cond_signal(pthread_cond_t *cond); |
pthread_cond_signal
唤醒该条件变量等待线程队列中的某一个线程
pthread_cond_broadcast
唤醒该条件变量等待线程队列中的所有线程,这些线程会进行竞争
代码示例
1 |
|
条件变量优点
相较于mutex而言,条件变量可以减少竞争。
如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
生成者消费者模型
一个生成者线程,一个消费者线程
生成者生成自定义的struct msg
生产的产品以链表的结构组合,头部插入,所以后生产的先消费
代码中生成者和消费者执行完各自动作后的睡眠不能去掉。不然某个线程解锁完立马加锁,一直占据CPU
1 |
|
进程同步
信号量sem
进化版的互斥锁(1 —> N)
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
信号量的操作
头文件:#include <semaphore.h>
- 定义信号量变量:
sem_t sem
- 初始化信号量变量:
sem_init
函数 - 对信号量加锁(P操作):
sem_wait
函数(阻塞)sem_trywait
函数(非阻塞)sem_timedlock
函数(限时加锁)
- 对信号量解锁(V操作):
sem_post
函数 - 销毁信号量变量:
sem_destroy
函数
以上5个函数的返回值都是:成功返回0;失败返回-1,并设置errno。
sem_t
类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。 初始化信号量sem不能 < 0。
信号量的初值,决定了可占用信号量的线程的个数。
函数说明
1)初始化信号量
1 | int sem_init(sem_t *sem, int pshared, unsigned int value); |
sem
信号量pshared
取 0用于线程间;取非0(一般为1)用于进程间value
指定信号量初值>=0
2)销毁信号量
1 | int sem_destroy(sem_t *sem); |
3)给信号量加锁 (P操作--
)
1 | int sem_wait(sem_t *sem); |
abs_timeout
绝对时间,参考前面pthread_mutex_timedlock
函数介绍
4)对信号量解锁 (V操作++
)
1 | int sem_post(sem_t *sem); |
生产者消费者模型
线程实现。一个生产者,一个消费者。所以只考虑了同步。如果是多个生产者,多个消费者,需要考虑上互斥。
1 |
|
互斥量mutex
进程间也可以使用互斥锁,来达到同步的目的。但应在pthread_mutex_init
初始化之前,修改其属性为进程间共享。mutex
的属性修改函数主要有以下几个。
主要操作
头文件:#include <pthread.h>
用于定义mutex锁的属性:
pthread_mutexattr_t mattr;
初始化一个mutex属性对象:
pthread_mutexattr_init
函数- 设置属性修改
mutex
:pthread_mutexattr_setpshared
函数 - 销毁
mutex
属性对象(而非销毁锁):pthread_mutexattr_destroy
函数
返回值:成功返回0,失败返回错误码
函数原型
1 | int pthread_mutexattr_init(pthread_mutexattr_t *attr); |
pshared
取值:
- 线程锁:
PTHREAD_PROCESS_PRIVATE
(mutex的默认属性即为线程锁,进程间私有) - 进程锁:
PTHREAD_PROCESS_SHARED
代码示例
1 |
|
条件变量cond
条件变量一般和互斥量配套使用。既然互斥量可以用于进程间,条件变量自然也可以。
头文件:#include <pthread.h>
用于定义条件变量cond的属性:
pthread_condattr_t mattr;
初始化一个cond属性对象:
pthread_condattr_init
函数- 设置属性修改cond:
pthread_condattr_setpshared
函数 - 销毁cond属性对象(而非销毁条件变量):
pthread_condattr_destroy
函数
返回值:成功返回0,失败返回错误码
函数原型
1 | int pthread_condattr_init(pthread_condattr_t *attr); |
pshared
取值:
- 线程:
PTHREAD_PROCESS_PRIVATE
(默认属性) - 进程:
PTHREAD_PROCESS_SHARED
读写锁rwlock
读写锁同样可以用于进程间互斥,需要设置属性
头文件:#include <pthread.h>
用于定义读写锁rwlock的属性:
pthread_rwlockattr_t mattr;
初始化一个rwlock属性对象:
pthread_rwlockattr_init
函数- 设置属性修改rwlock:
pthread_rwlockattr_setpshared
函数 - 销毁rwlock属性对象(而非销毁读写锁):
pthread_rwlockattr_destroy
函数
返回值:成功返回0,失败返回错误码
函数原型
1 | int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); |
pshared
取值:
- 线程:
PTHREAD_PROCESS_PRIVATE
(默认属性) - 进程:
PTHREAD_PROCESS_SHARED
文件锁fcntl
借助 fcntl
函数来实现锁机制。操作文件的进程没有获得锁时,可以打开,但无法执行read
、write
操作。
1 | int fcntl(int fd, int cmd, struct flock*); |
cmd
F_SETLK
设置文件锁(trylock)F_SETLKW
设置文件锁(lock),解锁(unlock)F_GETLK
获取文件锁
注意:
1)依然遵循读共享、写独占特性。但!如若进程不加锁直接操作文件,依然可访问成功,但数据势必会出现混乱。
2)多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量来实现的。因此,多线程中无法使用文件锁。
使用示例:
1 |
|
总结
进程和线程共有:
互斥量mutex、读写锁rwlock、条件变量cond、信号量sem、
进程独有:文件锁