12. 多线程编程
注:博客中有书中没有的内容,均是来自
黑马06-线程概念_哔哩哔哩_bilibili
早期Linux不支持线程,直到1996年,Xavier Leroy等人开发出第一个基本符合POSIX标准的线程库LinuxThreads,但LinuxThreads效率低且问题多,自内核2.6开始,Linux才开始提供内核级的线程支持,并有两个组织致力于编写新的线程库:NGPT(Next Generation POSIX Threads)和NPTL(Native POSIX Thread Library),但前者在2003年就放弃了,因此新的线程库就是NPTL。NPTL比LinuxThreads效率高,且更符合POSIX规范,所以它已经成为glibc的一部分,本书使用的线程库是NPTL。
本章要讨论的线程相关的内容都属于POSIX线程(简称 pthread)标准,不局限于NPTL实现,包括:
- 创建线程和结束线程;
- 读取和设置线程属性;
- POSIX线程同步方式:POSIX信号量、互斥锁和条件变量。
文章目录
- 12. 多线程编程
- 1.线程概念
- 1.什么是线程
- 2.Linux内核线程实现原理
- 3.线程共享资源
- 4.线程优缺点
- 2.Linux 线程概述
- 1.线程模型
- 2.Linux 线程库
- 3.线程控制原语
- 1.pthread_self
- 2.pthread_create
- 循环创建多个子线程
- 3.pthread_exit
- 4.pthreda_join
- 5pthread_detach
- 6.pthread_cancel
- 7.终止线程方式
- 8.控制原语对比
- 4.线程属性
- 1.概述
- 2.线程属性初始化和销毁
- 3.线程的分离状态
- 线程分离状态的函数
- 4.线程属性控制示例
- 5.线程使用注意事项
- 以下是线程同步的内容:
- 6.同步概念
- 同步
- 线程同步
- 7.互斥锁
- 1.概述
- 2.互斥锁基础 API
- 1.初始化和销毁
- 2.加锁解锁
- 3.使用案例
- 4.注意事项
- 5.try锁
- 3.互斥锁属性
- 4.死锁
- 8.读写锁
- 1.原理
- 2.特性
- 3.对应函数
- 1.初始化和销毁
- 2.加锁解锁
- 4.示例
- 9.条件变量
- 1.工作原理
- 2.对应函数
- 1.总览
- 2.创建和销毁
- 3.wait函数
- 4.pthread_cond_timedwait 函数
- 3.使用条件变量模拟实现生产者—消费者问题
- 4.条件变量优势
- 9.POSIX 信号量
- 1.概述
- 2.对应函数
- 1.总览
- 2.初始化和销毁
- 3.PV操作主要函数
- 3.实现生产者消费者
- 10.线程同步机制包装类
- 11.多线程环境
- 1.可重入函数
- 2.进程和线程
- 人话翻译一下它这三个函数在干什么
- 总结
- 3.线程和信号
1.线程概念
1.什么是线程
LWP:
light weight process 轻量级的进程,本质仍是进程(在Linux环境下)
进程:独立地址空间,拥有PCB
线程:有独立的PCB,但没有独立的地址空间(与进程共享)
区别:在于是否共享地址空间。独居(地址空间就是进程一个) ; 合租(地址空间有多个线程)
Linux 下:
线程:最小的执行(调度)单位
进程:最小分配资源单位,可看成是只有一个线程的进程
执行该命令可以得到线程号LWP(LWP是cpu执行的最小单位,但和后面讲到的线程id不是一回事)
ps aux
ps -Lf 进程id //得到该进程里面的线程
图中LWP就是线程号,可以看到是接着4388和4213往后写的,所以其实内核把这些进程的线程当做进程看,所以更容易获得CPU
2.Linux内核线程实现原理
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切
1.轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
2.从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
这句话中关于进程和线程都有各自不同的 PCB 部分正确,但线程的 PCB 相对进程的 PCB 有很大差异且不那么独立;而说进程和线程指向内存资源的三级页表相同是错误的,应该是一个进程内的线程共享进程的页表,不同进程之间页表是不同的。
3.进程可以蜕变成线程
4.线程可看做寄存器和的集合
5.在linux下,线程最是小的执行单位;进程是最小的分配资源单位
三级映射:进程PCB->页目录->页表->物理页面
3.线程共享资源
线程共享资源
-
1.文件描述符表
-
2.每种信号的处理方式
-
3.当前工作目录
-
4.用户ID和组 ID
-
5.内存地址空间( 具体共享的部分是: .text/.data/.bss/heap/共享库)------>线程间共享全局变量
线程非共享资源
-
1.线程id
-
2.处理器现场和栈指针(内核栈)
-
3.独立的空间(用户空间拔)
-
4.errno 变量
-
5.信号屏蔽字
-
6.调度优先级
不推荐信号和线程一起用,最好就别混着用。
4.线程优缺点
优点:
-
提高程序并发性
-
开销小
-
数据通信、共享数据方便
缺点:
-
库函数,不如系统调用稳定性强
-
调试、编写困难、gdb不支持
-
对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大
如果线程进程都可用,那优先选择线程
2.Linux 线程概述
1.线程模型
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。根据运行环境和调度者的身份,线程可分为内核线程和用户线程。
- 内核线程在有的系统上也称为LWP(Light Weight Process,轻量级进程),运行在内核空间,由内核调度;
- 用户线程运行在用户空间,由线程库调度。当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程,可见,内核线程相当于用户线程运行的容器,一个进程可以拥有M个内核线程和N个用户线程,其中M<=N,并且在一个系统的所有进程中,M和N的比值都是固定的。
按照M:N的取值,线程的实现可分为三种模式:完全在用户空间实现、完全由内核调度、双层调度(two level scheduler)。
完全在用户空间实现的线程无须内核的支持,内核甚至不知道这些线程的存在,线程库负责管理所有执行线程,如线程的优先级、时间片等。线程库利用longjmp
函数来切换线程的执行,使它们看起来像是并发执行的,但实际上内核仍然是把整个进程作为最小单位来调度,换句话说,一个进程中的所有线程共享该进程的时间片,它们对外表现出相同的优先级(即所有线程使用相同的优先级,因为它们都是以进程为单位调度的)。对这种实现方式而言,M=1,即 N 个用户线程对应一个内核线程,而该内核线程对实际就是进程本身。完全在用户空间实现的线程的优点:创建和调度线程都无需内核干预,因此速度快,且它不占用额外内核资源,所以即使一个进程创建了很多线程,也不会对系统性能造成明显的影响。其缺点是:对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上,因为内核是按照其最小调度单位来分配CPU的,且线程的优先级只在相对于进程内的其他线程生效,比较不同进程内的线程的优先级无意义。
完全由内核调度的模式将创建、调度线程的任务交给了内核,运行在用户空间的线程库无须执行管理任务,这与完全在用户空间实现的线程恰恰相反,因此二者的优缺点也正好互换。较早的Linux内核对内核线程的控制能力有限,线程库通常还要提供额外的控制能力,尤其是线程同步机制,但现代Linux内核已经大大增强了对线程的支持。完全由内核调度的线程实现满足M : N=1 : 1,即1个用户空间线程被映射为1个内核线程。
双层调度模式是两种实现模式的混合体,内核调度M个内核线程,线程库调度N个用户线程,这种线程实现方式结合了前两种方式的优点,不会消耗过多的内核资源,且线程切换速度也较快,同时还能充分利用多处理器优势。
完全在用户空间实现:一个内核级线程对多个用户级别线程
完全由内核调度:一个内核级线程对一个用户级线程
双层调度:多个内核级线程对多个用户级线程
2.Linux 线程库
Linux上两个有名的线程库是LinuxThreads和NPTL,它们都采用1:1方式实现(完全由内核调度的模式)。现代Linux上默认使用的线程库是NPTL,用户可用以下命令查看当前系统使用的线程库:
getconf GNU_LIBPTHREAD_VERSION
LinuxThreads线程库的内核线程是用clone
系统调用创建的进程模拟的,clone
系统调用和fork
系统调用的作用类似,都创建调用进程的子进程,但我们可以为clone
系统调用指定CLONE_THREA
D标志,此时它创建的子进程与调用进程共享相同的虚拟地址空间、文件描述符、信号处理函数,这些都是线程的特点,但用进程模拟内核线程会导致很多语义问题:
- 每个线程拥有不同的PID,不符合POSIX规范。
- Linux信号处理本来是基于进程的,但现在一个进程内部的所有线程都能且必须处理信号。
- 用户ID、组ID对一个进程中的不同线程来说可能不同。
- 进程产生的核心转储文件不会包含所有线程的信息,而只包含该核心转储文件的线程的信息。
- 由于每个线程都是一个进程,因此系统允许的最大进程数就是最大线程数。
LinuxThreads线程库一个有名的特性是所谓的管理线程,它是进程中专门用于管理其他工作线程的线程,其作用为:
- 系统发送给进程的终止信号先由管理线程接收,管理线程再给其他工作线程发送同样的信号以终止它们。
- 当终止工作线程或工作线程主动退出时,管理线程必须等待它们结束,以避免僵尸进程。
- 如果主线程即将先于其他工作线程退出,则管理线程将阻塞主线程,直到所有其他工作线程都结束后才唤醒它。
- 回收每个线程堆栈使用的内存。
管理线程的引入,增加了额外的系统开销,且由于管理线程只能运行在一个CPU上,所以LinuxThreads线程库不能充分利用多处理器系统的优势(所有管理操作只能在一个CPU上完成)。
要解决LinuxThreads线程库的一系列问题,不仅需要改进线程库,最主要的是需要内核提供更完善的线程支持,因此Linux内核从2.6版本开始,提供了真正的内核线程,新的NPTL线程库也应运而生,相比LinuxThreads,NPTL的主要优势在于:
- 内核线程不再是一个进程,因此避免了很多用进程模拟线程导致的语义问题。
- 摒弃了管理线程,终止线程、回收线程堆栈等工作都可以由内核完成。
- 由于不存在管理线程,所以一个进程的线程可以运行在不同CPU上,从而充分利用了多处理器系统的优势。
- 线程的同步由内核来完成,隶属于不同进程的线程之间也能共享互斥锁,因此可实现跨进程的线程同步。
3.线程控制原语
创建和结束线程的API在Linux上定义在pthread.h
头文件。
1.pthread_self
获取当前线程的线程ID。其作用对应进程中getpid()函数。
#include<pthread.h>
pthread_t pthread_self(void);
返回值:
成功:0;失败:无 !
线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现。
线程ID是进程内部识别线程的标志。(两个进程间,线程ID允许相同)
注意:不应使用全局变量 pthread_t tid,在子线程中通过 pthread_create 传出参数来获取线程 ID,而应使用pthread self
2.pthread_create
pthread_create
函数创建一个线程:
#include <pthread.h>
int pthread_create(pthread_t* thread, count pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
参数
thread
:传出参数,表示新创建的子线程的线程id,新线程的标识符,后续pthread_*
函数通过它来引用新线程,其类型pthread_t
定义如下:
#include <bits/pthreadtypes.h>
typedef unsignde long int ptherad_t;
attr
:用于设置新线程的属性,给它传递NULL
表示使用默认线程属性。
start_routine
:子线程的回调函数
arg
:子线程回调函数的参数,没有的话传一个NULL
返回值
成功时返回0,失败时返回错误码,并且第一个参数thread不会有值。
一个用户可以打开的线程数不能超过RLIMIT_NPROC
软资源限制,此外,系统上所有用户能创建的线程总数也不能超过/proc/sys/kernel/threads-max
内核参数定义的值。
循环创建多个子线程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include<pthread.h>void *tfn(void *arg)
{int i=(int)arg;sleep(i);printf("I'm %dth thread:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());return NULL;
}int main(int argc,char* argv[])
{int i,ret;pthread_t tid;for(i=0;i<5;i++){ret=pthread_create(&tid,NULL,tfn,(void*)i);//ret=pthread_create(&tid,NULL,tfn,(void*)&i);//这样写是不行的,因为传入的是i的地址,但是i是循环变量,一直在变化,可能现在传入的是2,但是等子线程读取的时候就是3或4了if(ret!=0){perror("pthread_create error");exit(1);}}sleep(i);printf("main: pid=%d,tid=%d\n",getpid(),pthread_self());return 0;
}
arg传值采用值传递,借助强转
3.pthread_exit
线程一旦被创建,内核就可以调度内核线程来执行start_routine
函数指针参数所指向的函数了,线程函数在结束时最好调用pthread_exit
函数,以确保安全、干净地退出:
#include <pthread.h>
void pthread_exit(void* retval);
pthread_exit
函数会通过retval
参数向线程的回收者传递其退出信息,它执行完后不会返回到调用者,且永远不会失败。
参数:
retval:回调函数的返回值。 无返回值时,传NULL
辨析:
exit(); 退出当前进程
return: 返回到调用者那里去
pthread_exit(): 退出当前线程
4.pthreda_join
一个进程中的所有线程都能调用pthread_join
函数来回收其他线程(前提是目标线程是可回收的),即等待其他线程结束,这类似回收进程的wait
和waitpid
系统调用。
阻塞等待线程退出,获取线程状态。
#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);
参数
thread
:目标线程的线程号(标识符)。
retval
:传出参数,目标线程返回的退出信息。就是接收pthread_exit的参数。
返回值
pthread_join
函数会一直阻塞,直到被回收的线程结束为止。
成功时返回0,失败则返回错误码。
使用示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include<pthread.h>struct thrd{int var;char str[256];
};void *tfn(void *arg)
{ struct thrd *tval;tval=malloc(sizeof(tval));tval->var=100;strcpy(tval->str,"hello pthread\n");return (void*)tval;
}int main(int argc,char* argv[])
{int i,ret;pthread_t tid;struct thrd *retval;ret=pthread_create(&tid,NULL,tfn,(void*)i);if(ret!=0){perror("pthread_create error");exit(1);}ret=pthread_join(tid,(void **)&retval);if(ret!=0){perror("pthread_join error");exit(1);}printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);return 0;
}
5pthread_detach
设置线程分离。当一个线程处于分离状态时,它的资源在结束时会自动被系统回收,而不需要其他线程调用pthread_join
函数来等待它结束并回收资源。
int pthread_detach(pthread_t thread);
参数:
thread: 待分离的线程id
返回值:
成功:0
失败:errno
非分离状态(默认情况):在默认情况下,线程是处于非分离状态(也称为可连接状态)。当一个线程创建后,如果不进行pthread_detach
操作,那么它的资源(如线程栈等)在结束后不会自动释放,需要其他线程通过pthread_join
函数来等待它结束,并且在pthread_join
函数返回后,系统才会回收该线程的资源。这就好像线程结束后,它的 “后事”(资源回收)需要其他线程来帮忙处理。
分离状态:使用pthread_detach
函数将线程设置为分离状态后,线程结束时就会自动释放自身的资源。这类似于线程自己处理自己的 “后事”,不需要其他线程专门等待它并回收资源。例如,在一些简单的多线程应用场景中,对于那些执行完任务后就不需要再关注其结果的线程,将它们设置为分离状态可以简化程序的逻辑,避免因为忘记调用pthread_join
而导致资源泄漏。
1.进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资
源仍存于系统中,导致内核认为该进程仍存在
2.**不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。**也就是说,如果已
经对一个线程调用了pthread_detach就不能再调用pthread_join了。
3.也可使用 pthread_create函数参2(线程属性)来设置线程分离
6.pthread_cancel
有时候我们希望异常终止一个线程,即取消(杀死)线程,它是通过pthread_cancel
函数实现的:
#include <ptrhead.h>
int pthread_cancel(pthread_t thread);
参数
thread
:要杀死的线程的线程号(标识符)
返回值
pthread_cancel
函数成功时返回0,失败则返回错误码。
接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这通过以下函数完成:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
注意:
线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是
立刻就能完成,必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用 creat,open,pause,
close,read,write … 执行命令man 7 pthreads 可以查看具备这些取消点的系统调用列表。。心
**可粗略认为一个系统调用(进入内核)即为一个取消点。**如线程中没有取消点,可以通过调用 pthread_testcancel
函数自行设置一个取消点
被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h
中找到它的定义:#define PTHREAD_CANCELED((void*)-1)。因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1
示例:
如代码所示,如果tfn中没有设置保存点,也没有进入内核的语句(只是一些for if之类的),那cancel就会失效的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>void *tfn(void *arg)
{while(1){/*printf("I'm thread,pid:%d,tid:%lu\n",getpid(),pthread_self());sleep(1);*/pthread_testcancel();//设置保存点}return NULL;
}int main(void)
{pthread_t tid;//创建线程int ret = pthread_create(&tid,NULL,tfn,NULL);if(ret!=0){perror("pthread_create error");exit(1);}printf("I'm main,pid:%d,tid:%lu\n",getpid(),pthread_self());// 终止线程ret = pthread_cancel(tid);while(1);pthread_exit(NULL);
}
7.终止线程方式
总结:终止某个线程而不终止整个进程,有三种方法:
-
从线程主函数 return。这种方法对主控线程不适用,从 main 函数 return 相当于调用 exit。
-
一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。
-
线程可以调用 pthread_exit 终止自己。
8.控制原语对比
4.线程属性
1.概述
pthread_attr_t
结构体定义了一套完整的线程属性:
#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union{char __size[__SIZEOF_PTHREAD_ATTR_T];long int __align;
} pthread_attr_t;
线程的各种属性都包括在一个字符数组里,线程库定义了一系列函数来操作pthread_attr_t
类型变量,方便我们设置和获取线程属性:
#include <pthread.h>int pthread_attr_init ( pthread_attr_t* attr ); /* 初始化线程属性对象 */
int pthread_attr_destroy(pthread_attr_t* attr ); /* 销毁线程属性对象。被销毁的线程属性对象只有再次初始化之后才能继续使用 *//* 下面这些函数用于获取和设置线程属性对象的某个属性 */
int pthread_attr_getdetachstate ( const pthread_attr_t* attr, int* detachstate );
int pthread_attr_setdetachstate( pthread_attr_t* attr, int detachstate );
int pthread_attr_getstackaddr( const pthread_attr_t* attr,void ** stackaddr );
int pthread_attr_setstackaddr( pthread_attr_t* attr, void* stackaddr );
int pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize );
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);
int pthread_attr_getstack ( const pthread_attr_t* attr, void** stackaddr, size_t* stacksize);/* 还有很多,不在这里一一列举了,具体可查看书籍P273页 */
linux 下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param
schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小int stackaddr_set; //线程的栈设置void* stackaddr; //线程栈的位置size_t stacksize; //线程栈的大小} pthread_attr_t;
主要结构体成员:
-
线程分离状态
-
线程栈大小(默认平均分配)
-
线程栈警戒缓冲区大小(位于栈末尾)
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init,这个函数必须在
pthread_create 函数之前调用。之后须用 pthread_attr_destroy 函数来释放资源。
**线程属性主要包括如下属性:**作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
最重要的部分,不过太麻烦了,不如detach
//设置分离属性
pthread_attr_t attr 创建一个线程属性结构体变量pthread_attr_init(&attr); 初始化线程属性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程pthread_attr_destroy(&attr); 销毁线程属性
2.线程属性初始化和销毁
注意:应先初始化线程属性,再 pthread_create 创建线程
初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
成功:0;失败:错误号
销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t *attr);
成功:0;失败:错误号
3.线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态:
线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当 pthread_join()
函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:
分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该
根据自己的需要,选择适当的分离状态。
线程分离状态的函数
设置线程属性,分离 or 非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
获取程属性,分离 or 非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
参数:
attr:已初始化的线程属性
detachstate: PTHREAD_CREATE_DETACHED(分离线程)
PTHREAD _CREATE_JOINABLE(非分离线程)
这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timedwait 函数,让这个线程等待一会儿,留出足够的时间让函数 pthread_create 返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如 wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
//设置分离属性
pthread_attr_t attr 创建一个线程属性结构体变量pthread_attr_init(&attr); 初始化线程属性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程pthread_attr_destroy(&attr); 销毁线程属性
4.线程属性控制示例
//设置分离属性
pthread_attr_t attr 创建一个线程属性结构体变量pthread_attr_init(&attr); 初始化线程属性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程pthread_attr_destroy(&attr); 销毁线程属性
#include <pthread.h>#define SIZE 0x100000void *th_fun(void *arg)
{while(1);sleep(1);
}int main(void)
{pthread_t tid;int err, detachstate, i = 1;pthread_attr_t attr;size_t stacksize;void *stackaddr;pthread_attr_init(&attr);pthread_attr_getstack(&attr, &stackaddr, &stacksize);pthread_attr_getdetachstate(&attr, &detachstate);if (detachstate == PTHREAD_CREATE_DETACHED)printf("thread detached\n");else if (detachstate == PTHREAD_CREATE_JOINABLE)printf("thread join\n");elseprintf("thread unknown\n");pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);while (1) {stackaddr = malloc(SIZE);if (stackaddr == NULL) {perror("malloc");exit(1);}stacksize = SIZE;pthread_attr_setstack(&attr, stackaddr, stacksize);err = pthread_create(&tid, &attr, th_fun, NULL);if (err != 0) {printf("%s\n", strerror(err));exit(1);}printf("%d\n", i++);}pthread_attr_destroy(&attr);return 0;
}
5.线程使用注意事项
1.主线程退出其他线程不退出,主线程应调用pthread_exit
2.避免僵尸线程
-
pthread_join
-
pthread_detach
-
pthread_create 指定分离属性
被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3.malloc和mmap申请的内存可以被其他线程释放
4.应避免在多线程模型中调用fork除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程
中均pthread_exit
5.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
以下是线程同步的内容:
6.同步概念
同步
所谓同步,即同时起步,协调一致。
不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等。
而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。
线程同步
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据
一致性,不能调用该功能。
举例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)。
为了避免这种数据混乱,线程需要同步。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信
号间等等都需要同步机制。因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。
7.互斥锁
1.概述
数据混乱原因:
-
资源共享(独享资源则不会)
-
调度随机(意味着数据访问会出现竞争)
-
线程间缺乏必要的同步机制。
以上 3 点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
互斥锁(也称互斥量)用于保护关键代码段,以确保其独占式的访问,这有些像二进制信号量(信号量),当进入关键代码段时,我们需要获得互斥锁并将其加锁,这等价于二进制信号量的P操作;当离开关键代码段时,我们需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程,这相当于二进制信号量的V操作。
注意:同一时刻,只能有一个线程持有该锁
当 A 线程对某个全局变量加锁访问,B 在访问前尝试加锁,拿不到锁,B 阻塞。C 线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
因此,即使有了 mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
2.互斥锁基础 API
#include <pthread.h>
int pthread_mutex_init ( pthread_mutex_t* mutex, /* 初始化互斥锁 */const pthread_mutexattr_t* mutexattr );
int pthread_mutex_destory ( pthread_mutex_t* mutex ); /* 销毁互斥锁 */
int pthread_mutex_lock ( pthread_mutex_t* mutex ); /* 以原子操作的方式给一个互斥锁加锁 */
int pthread_mutex_trylock ( pthread_mutex_t* mutex ); /* 相当于 pthread_mutex_lock 的非阻塞版本 ,非阻塞轮询*/
int pthread_mutex_unlock ( pthread_mutex_t* mutex ); /* 以原子操作的方式给一个互斥锁解锁 */
以上5个函数的返回值都是:成功返回0,失败返回错误号。
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex;
变量 mutex只有两种取值1、0
使用mutex(互斥量、互斥锁)一般步骤:
pthread_mutex_t 类型。 1. pthread_mutex_t lock; 创建锁2 pthread_mutex_init; 初始化 13. pthread_mutex_lock;加锁 1-- --> 04. 访问共享数据(stdout) 5. pthrad_mutext_unlock();解锁 0++ --> 16. pthead_mutex_destroy;销毁锁
1.初始化和销毁
int pthread_mutex_init ( pthread_mutex_t* mutex, /* 初始化互斥锁 */const pthread_mutexattr_t* mutexattr );
参数:
mutex:咱们创建的锁
mutexattr:锁的属性
int pthread_mutex_destory ( pthread_mutex_t* mutex ); /* 销毁互斥锁 */
参数同上
restrict关键字,用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); //动态初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化。
2.加锁解锁
int pthread_mutex_lock ( pthread_mutex_t* mutex ); /* 以原子操作的方式给一个互斥锁加锁 */
int pthread_mutex_trylock ( pthread_mutex_t* mutex ); /* 相当于 pthread_mutex_lock 的非阻塞版本 ,非阻塞轮询*/
int pthread_mutex_unlock ( pthread_mutex_t* mutex ); /* 以原子操作的方式给一个互斥锁解锁 */
参数都是我们创建的锁
3.使用案例
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>pthread_mutex_t mutex;void err_thread(int ret, char *str)
{if (ret != 0) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);}
}void *tfn(void *arg)
{srand(time(NULL));while (1) {pthread_mutex_lock(&mutex);printf("hello ");sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/printf("world\n");pthread_mutex_unlock(&mutex);sleep(rand() % 3);}return NULL;
}int main(void)
{int flag = 5;pthread_t tid;srand(time(NULL));pthread_mutex_init(&mutex, NULL);pthread_create(&tid, NULL, tfn, NULL);while (flag--) {pthread_mutex_lock(&mutex);printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");pthread_mutex_unlock(&mutex);sleep(rand() % 3);}pthread_cancel(tid); // 将子线程杀死,子线程中自带取消点pthread_join(tid, NULL);pthread_mutex_destroy(&mutex);return 0; //main中的return可以将整个进程退出
}
4.注意事项
1.尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束立即解锁。)
2.互斥锁,本质是结构体。 我们可以看成整数。 可以认为是初值为 1。(pthread_mutex_init() 函数调用成功。)
3.加锁: --操作, 阻塞线程。
4.解锁: ++操作, 换醒阻塞在锁上的线程。
5.try锁
int pthread_mutex_trylock ( pthread_mutex_t* mutex ); /* 相当于 pthread_mutex_lock 的非阻塞版本 ,非阻塞轮询*/
lock加锁失败会阻塞,等待锁释放
trylock不断尝试加锁,加锁失败直接返回错误号(如:EBUSY),不阻塞
3.互斥锁属性
pthread_mutex_t
结构体描述互斥锁的属性,线程库提供了一系列函数来操作pthread_mutexattr_t
类型的变量,以方便我们获取和设置互斥锁属性,以下是其中一些主要的函数:
#include <pthread.h>/* 初始化互斥锁属性对象 */
int pthread_mutexattr_init ( pthread_mutexattr_t* attr );/* 销毁互斥锁属性对象 */
inrt pthread_mutexattr_destroy ( pthread_mutexattr_t* attr );/* 获取和设置互斥锁的 pshared 属性 */
int pthread_mutexattr_getpshared ( const pthread_mutexattr_t* attr, int* pshared );
int pthread_muextattr_setpshared ( pthread_mutexattr_t* attr, int* pshared );/* 获取和设置互斥锁的 type 属性 */
int pthread_mutexattr_gettype ( const pthread_mutexattr_t* attr, int* type );
int pthread_mutexattr_settype ( pthread_mutexattr_t* attr, int* type );
本书仅讨论互斥锁的两种常用属性:pshared
和type
。
互斥锁属性pshared
指定是否允许跨进程共享互斥锁,其可选值为:
PTHREAD_PROCESS_SHARED
:互斥锁可以被跨进程共享。PTHREAD_PROCESS_PRIVATE
:互斥锁只能和锁的初始化线程隶属于同一个进程的线程共享。
互斥锁属性type
指定互斥锁的类型,Linux支持以下4种互斥锁:
PTHREAD_MUTEX_NORMAL
:普通锁,这是互斥锁的默认类型。当一个线程对一个普通锁加锁后,其余请求该锁的线程将形成一个等待队列,并在该锁解锁后按优先级获得它。这种锁类型保证了资源分配的公平性,但也容易引发问题:一个线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。PTHREAD_MUTEX_ERRORCHECK
:检错锁。一个线程如果对一个自己加锁的检错锁再次加锁,则加锁操作返回EDEADLK
。对一个已经被其他线程加锁的检错锁解锁,或对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM
。PTHREAD_MUTEX_RECURSIVE
:嵌套锁。这种锁允许一个线程在释放锁前多次对它加锁而不发生死锁,但如果其他线程要获得这个锁,则当前锁的拥有者必须执行相应次数的解锁操作。对一个已经被其他线程加锁的嵌套锁解锁,或对一个已经解锁的嵌套锁再次解锁,则解锁操作将返回EPERM
。PTHREAD_MUTEX_DEFAULT
:默认锁。通常被映射为以上三种锁之一。
4.死锁
是使用锁不恰当造成的现象:
-
线程试图对同一个互斥量A加锁两次。
-
线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
死锁使一个或多个线程被挂起而无法继续执行,且这种情况还不容易被发现。
在一个线程中对一个已经加锁的普通锁再次加锁将导致死锁。另外,如果两个线程按照不同顺序来申请两个互斥锁,也容易产生死锁,如以下代码所示:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;void *another (void *arg) {pthread_mutex_lock(&mutex_b); /* 子线程上锁 mutex_b */printf("in child thread, got mutex b, waiting for mutex a\n");sleep(5);++b;pthread_mutex_lock(&mutex_a); /* 子线程上锁 mutex_a */b += a++;pthread_mutex_unlock(&mutex_a); /* 解锁 */pthread_mutex_unlock(&mutex_b);pthread_exit(NULL);
}int main () {pthread_t id;pthread_mutex_init(&mutex_a, NULL); /* 初始化互斥锁 */pthread_mutex_init(&mutex_b, NULL);pthread_create(&id, NULL, another, NULL); /* 创建线程 */pthread_mutex_lock(&mutex_a); /* 主线程上锁 mutex_a */printf("in parent thread, got mutex a, waiting for mutex b\n");sleep(5);++a;pthread_mutex_lock(&mutex_b); /* 主线程上锁 mutex_b */a += b++;pthread_mutex_unlock(&mutex_b);pthread_mutex_unlock(&mutex_a);/* 主线程等待子线程结束,然后销毁互斥锁以释放资源 */pthread_join(id, NULL);pthread_mutex_destroy(&mutex_a);pthread_mutex_destroy(&mutex_b);return 0;
}
由于两个线程都在等待对方已经持有的锁释放,因此会发生死锁,两个线程都将永远等待下去。 为了避免死锁,应确保所有线程以相同的顺序获取互斥锁。
编译:-lpthread
选项确保链接了 POSIX 线程库。
g++ -o test test.cpp -lpthread
8.读写锁
1.原理
1.锁只有一把。以读方式给数据加锁,那锁就是读锁,以写方式给数据加锁,那锁就是写锁。
2.读共享,写独占。
3.写锁优先级高。
-
如果有五个进程同时请求锁,1个写请求4个读请求,那么优先给写锁。
-
如果4个读请求比写请求先到,并且已经加锁成功,那么不会断开读请求的进程给写请求的进程锁的。
-
**如果读锁和写锁在同一队列阻塞等待,那么优先给写锁:**如果进程1是读进程已经加锁成功在读了,后边同时来了3个进程,2,4进程写请求,3进程读请求,这个时候是这样的:1读完之后,2和4写,写完之后3再读(写优先级高)
2.特性
-
读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞。
-
读写锁是“读模式加锁”时,
-
读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁
会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
**读写锁也叫共享-独占锁。**当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独
占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
相较于互斥量而言,当读线程多的时候,提高访问效率
3.对应函数
pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL);pthread_rwlock_rdlock(&rwlock);
pthread_rwlock_wrlock(&rwlock); pthread_rwlock_tryrdlock(&rwlock);
pthread_rwlock_trywrlock(&rwlock); pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
以上都是成功返回0失败返回错误号
pthread_rwlock_t类型 用于定义一个读写锁变量。
pthread rwlock t rwlock;
1.初始化和销毁
pthread_rwlock_init 函数
初始化一把读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
参 2:attr 表读写锁属性,通常使用默认属性,传 NULL 即可。
pthread_rwlock_destroy 函数
销毁一把读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
2.加锁解锁
加锁
pthread_rwlock_rdlock 函数
以读方式请求读写锁。(常简称为:请求读锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock 函数
以写方式请求读写锁。(常简称为:请求写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock 函数
非阻塞以读方式请求读写锁(非阻塞请求读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock 函数
非阻塞以写方式请求读写锁(非阻塞请求写锁)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
解锁
pthread_rwlock_unlock 函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
4.示例
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */#include <stdio.h>
#include <unistd.h>
#include <pthread.h>int counter; //全局资源
pthread_rwlock_t rwlock;void *th_write(void *arg)
{int t;int i = (int)arg;while (1) {t = counter; // 保存写之前的值usleep(1000);pthread_rwlock_wrlock(&rwlock);printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);pthread_rwlock_unlock(&rwlock);usleep(9000); // 给 r 锁提供机会}return NULL;
}void *th_read(void *arg)
{int i = (int)arg;while (1) {pthread_rwlock_rdlock(&rwlock);printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);pthread_rwlock_unlock(&rwlock);usleep(2000); // 给写锁提供机会}return NULL;
}int main(void)
{int i;pthread_t tid[8];pthread_rwlock_init(&rwlock, NULL);for (i = 0; i < 3; i++)pthread_create(&tid[i], NULL, th_write, (void *)i);for (i = 0; i < 5; i++)pthread_create(&tid[i+3], NULL, th_read, (void *)i);for (i = 0; i < 8; i++)pthread_join(tid[i], NULL);pthread_rwlock_destroy(&rwlock); //释放读写琐return 0;
}
9.条件变量
如果说互斥锁是用于同步线程对共享数据的访问,那么条件变量则是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的线程。
条件变量是多线程编程中用于同步的一种机制,它允许线程在某些条件未被满足时暂停执行,并在条件满足时被唤醒继续执行。条件变量通常与互斥锁(mutexes)一起使用,以协调对共享资源的访问。这是一种避免忙等(busy-waiting)并减少CPU资源浪费的有效方式。
1.工作原理
- 等待条件变量:当线程需要访问某个共享资源,但条件不满足时,它会通过互斥锁保护条件变量,并在该条件变量上等待。在这个等待过程中,线程会释放互斥锁,以便其他线程可以修改这个条件。
- 唤醒等待的线程:其他线程在修改了条件之后,可以通过条件变量来唤醒一个或多个正在等待这个条件的线程。
- 重新检查条件:被唤醒的线程会重新获取互斥锁,并再次检查条件是否满足。如果条件满足,线程继续执行;如果不满足,线程可能会再次等待。
2.对应函数
1.总览
条件变量的相关函数如下:
#incldue <pthread.h>/* 初始化条件变量 */
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);/* 销毁条件变量 */
int pthread_cond_destroy (pthread_cond_t* cond);/* 以广播方式唤醒所有等待目标条件的线程 */
int pthread_cond_broadcast (pthread_cond_t* cond);/* 唤醒一个等待目标条件变量的线程,唤醒哪个线程取决于线程的优先级和调度策略 */
int pthread_cond_signal (pthread_cond_t* cond);/* 等待目标条件变量 */
int pthread_cond_wait (ptread_cond_t* cond, pthread_mutex_t* mutex);/*限时等待一个条件变量*/
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
pthread_cond_t 类型 用于定义条件变量
pthread_cond_t cond;
成功返回0失败直接返回错误号
pthread_cond_signal(): 唤醒阻塞在条件变量上的 (至少)一个线程。
pthread_cond_broadcast(): 唤醒阻塞在条件变量上的 所有线程。
2.创建和销毁
创建
/* 初始化条件变量 */
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
参 2:attr 表条件变量属性,通常为默认值,传 NULL 即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
销毁
/* 销毁条件变量 */
int pthread_cond_destroy (pthread_cond_t* cond);
3.wait函数
阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数
cond:条件变量
mutex:互斥锁
函数作用:
-
阻塞等待条件变量 cond(参 1)满足
-
释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
**1.2.**两步为一个原子操作。
- 当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);
这张图是对wait过程的说明:
1.锁是提前创建和初始化好的,然后加锁
2.调用pthread_cond_wait,看条件变量是否满足
3.不满足就阻塞等待,阻塞等待的时候就把锁给解了,让别人用去了(判断阻塞和解锁这两步是一个原子操作)
4.等到满足条件变量满足的时候,再申请重新加锁(重新加锁是条件变量内部实现,不需要咱们自己加锁)
4.pthread_cond_timedwait 函数
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参 3:
参看 man sem_timedwait 函数,查看 struct timespec 结构体。
struct timespec {
time_t tv_sec;
/* seconds */ 秒
long tv_nsec;
/* nanosecondes*/ 纳秒
}
形参 abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而 alarm(1)是相对时间,相对当前时间定时 1 秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970 年 1 月 1 日 00:00:01 秒(早已经过去)
正确用法:
time_t cur = time(NULL); //获取当前时间。struct timespec t; //定义 timespec 结构体变量 tt.tv_sec = cur+1; //定时 1 秒pthread_cond_timedwait (&cond, &mutex, &t); //传参
在讲解 setitimer 函数时我们还提到另外一种时间类型:
struct timeval {
time_t tv_sec; /* seconds */ 秒
susecods_t tv_usec; /* microseconds */ 微秒
};
3.使用条件变量模拟实现生产者—消费者问题
流程
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>void err_thread(int ret, char *str)
{if (ret != 0) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);}
}//魔方公共区域的链表
struct msg {int num;struct msg *next;
};struct msg *head;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量void *produser(void *arg)
{while (1) {struct msg *mp = malloc(sizeof(struct msg));mp->num = rand() % 1000 + 1; // 模拟生产一个数据`printf("--produce %d\n", mp->num);pthread_mutex_lock(&mutex); // 加锁 互斥量mp->next = head; // 写公共区域head = mp;pthread_mutex_unlock(&mutex); // 解锁 互斥量pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量 has_data上的线程.sleep(rand() % 3);}return NULL;
}void *consumer(void *arg)
{while (1) {struct msg *mp;pthread_mutex_lock(&mutex); // 加锁 互斥量while (head == NULL) {pthread_cond_wait(&has_data, &mutex); // 阻塞等待条件变量, 解锁} // pthread_cond_wait 返回时, 重新加锁 mutexmp = head;head = mp->next;pthread_mutex_unlock(&mutex); // 解锁 互斥量printf("---------consumer id: %lu :%d\n", pthread_self(), mp->num);free(mp);sleep(rand()%3);}return NULL;
}int main(int argc, char *argv[])
{int ret;pthread_t pid, cid;srand(time(NULL));ret = pthread_create(&pid, NULL, produser, NULL); // 生产者if (ret != 0) err_thread(ret, "pthread_create produser error");ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者if (ret != 0) err_thread(ret, "pthread_create consuer error");ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者if (ret != 0) err_thread(ret, "pthread_create consuer error");ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者if (ret != 0) err_thread(ret, "pthread_create consuer error");pthread_join(pid, NULL);pthread_join(cid, NULL);return 0;
}
在公共区域为空的时候,消费者都会阻塞等待hasdata这个条件变量,都会把锁释放掉,而生产者这时会拿到锁进行生产,生产者生产完唤醒消费者进行消费。
4.条件变量优势
相较于 mutex 而言,条件变量可以减少竞争。
如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
9.POSIX 信号量
1.概述
在Linux上,信号量API有两组,一组是System V IPC信号量(信号量),另一组是我们要讨论的POSIX信号量。这两组接口很相似,且语义完全相同,但不保证能互换。
进化版的互斥锁(1 --> N)
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办
法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导
致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发
POSIX信号量函数的名字都以sem_
开头,不像大多线程函数那样以pthread_
开头。常用的POSIX信号量函数如下:
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value); /* 初始化一个未命名信号量 */
int sem_destory(sem_t* sem); /* 销毁信号量 */
int sem_wait(sem_t* sem); /* 以原子操作的方式将信号量的值减1 */
int sem_trywait(sem_t *sem); /* 相当于sem_wait函数的非阻塞版本 */
int sem_post(sem_t *sem); /* 以原子操作的方式将信号量的值加1 */
上图中函数的第一个参数sem
指向被操作的信号量。
sem_init
函数用于初始化一个未命名信号量(POSIX信号量API支持命名信号量,但本书不讨论)。pshared
参数指定信号量类型,如果值为0,就表示这个信号量是当前进程的局部信号量,否则该信号量可以在多个进程间共享。value
参数指定信号量的初始值。初始化一个已经被初始化的信号量将导致不可预期的结果。sem_destroy
函数用于销毁信号量,以释放其占用的内核资源,销毁一个正被其他线程等待的信号量将导致不可预期的结果。sem_wait
函数以原子操作的方式将信号量的值减1,如果信号量的值为0,则sem_wait
函数将被阻塞,直到这个信号量具有非0值。sem_trywait
函数与sem_wait
函数类似,但它始终立即返回,而不论被操作的信号量是否具有非0值,相当于sem_wait
函数的非阻塞版本。当信号量非0时,sem_trywait
函数对信号量执行减1操作,当信号量的值为0时,该函数返回-1并设置errno为EAGAIN
。sem_post
函数以原子操作的方式将信号量的值加1,当信号量的值从0变为1时,其他正在调用sem_wait
等待信号量的线程将被唤醒。
上图中的函数成功时返回0,失败则返回-1并设置errno。
2.对应函数
1.总览
#include<semaphore.h>
sem_t sem;
sem_init 函数
sem_destroy 函数
sem_wait 函数
sem_trywait 函数
sem_timedwait 函数
sem_post 函数
以上 6 个函数的返回值都是:成功返回 0, 失败返回-1,同时设置 errno。(注意,它们没有 pthread 前缀)
sem_t 类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
规定信号量 sem 不能 < 0。
信号量基本操作:
sem_wait:
1.信号量大于 0,则信号量-- (类比 pthread_mutex_lock)
2.信号量等于 0,造成线程阻塞
对应
sem_post:
将信号量++,同时唤醒阻塞在信号量上的线程 (类比 pthread_mutex_unlock)
但,由于 sem_t 的实现对用户隐藏,所以所谓的++、–操作只能通过函数来实现,而不能直接++、–符号。
信号量的初值,决定了占用信号量的线程的个数。
2.初始化和销毁
初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参 1:sem 信号量
参 2:pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
参 3:value 指定信号量初值
sem_destroy 函数
销毁一个信号量
int sem_destroy(sem_t *sem);
3.PV操作主要函数
sem_wait 函数
给信号量加锁 –
int sem_wait(sem_t *sem);
sem_post 函数
给信号量解锁 ++
int sem_post(sem_t *sem);
sem_trywait 函数
尝试对信号量加锁 – (与 sem_wait 的区别类比 lock 和 trylock)
int sem_trywait(sem_t *sem);
sem_timedwait 函数
限时尝试对信号量加锁 –
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
参 2:abs_timeout 采用的是绝对时间。
定时 1 秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义 timespec 结构体变量 t
t.tv_sec = cur+1; 定时 1 秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参
3.实现生产者消费者
流程
完整代码
/*信号量实现 生产者 消费者问题*/#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, 线程间共享 -- 0sem_init(&product_number, 0, 0); //产品数为0pthread_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;
}
10.线程同步机制包装类
为了充分复用代码,同时后文需要,我们将前面讨论的三种线程同步机制分别封装为三个类,实现在locker.h
头文件中:
#ifndef LOCKER_H
#define LOCKER_H#include <exception>
#include <pthread.h>
#include <semaphore.h>// 封装信号量的类
class sem {
public:// 创建并初始化信号量sem() {if (sem_init(&m_sem, 0, 0) != 0) {// 构造函数没有返回值,可通过抛出异常来报告错误throw std::exception();}}// 销毁信号量~sem() {sem_destroy(&m_sem);}// 等待信号量bool wait() {return sem_wait(&m_sem) == 0;}// 增加信号量bool post() {return sem_post(&m_sem) == 0;}private:sem_t m_sem;
};// 封装互斥锁的类
class locker {
public:// 创建并初始化互斥锁locker() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw std::exception();}}// 销毁互斥锁~locker() {pthread_mutex_destroy(&m_mutex);}// 获取互斥锁bool lock() {return pthread_mutex_lock(&m_mutex) == 0;}// 释放互斥锁bool unlock() {return pthread_mutex_unlock(&m_mutex) == 0;}private:pthread_mutex_t m_mutex;
};// 封装条件变量的类
class cond {
public:// 创建并初始化条件变量cond() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw std::exception();}if (pthread_cond_init(&m_cond, NULL) != 0) {// 构造函数中一旦出现问题,就应立即释放已经成功分配的资源pthread_mutex_destroy(&m_mutex);throw std::exception();}}// 销毁条件变量~cond() {pthread_mutex_destroy(&m_mutex);pthread_cond_destroy(&m_cond);}// 等待条件变量bool wait() {int ret = 0;// 作者在此处对互斥锁加锁,保护了什么?这导致其他人无法使用该封装类pthread_mutex_lock(&m_mutex);ret = pthread_cond_wait(&m_cond, &m_mutex);pthread_mutex_unlock(&m_mutex);return ret == 0;}// 唤醒等待条件变量的线程bool signal() {return pthread_cond_signal(&m_cond) == 0;}private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;
};#endif
11.多线程环境
1.可重入函数
如果一个函数能被多个线程同时调用且不发生竞态条件,则我们称它是线程安全的(thread safe),或者说它是可重入函数。Linux库函数只有一小部分是不可重入的。这些库函数之所以不可重入,主要是因为其内部使用了静态变量,但Linux对很多不可重入的库函数提供了对应的可重入版本,这些可重入版本的函数名是在原函数名尾部加上_r
,如localtime
函数对应的可重入函数是localtime_r
。
在多线程程序中调用库函数,一定要使用其可重入版本,否则可能导致预想不到的结果。
2.进程和线程
多线程环境中,使用fork
调用产生的死锁问题
如果多线程的某个线程(可以理解为一个进程)调用了fork
函数,那么新创建的子进程只拥有一个执行线程,它是调用fork
的那个线程的完整复制,且子进程将自动继承父进程中互斥锁、条件变量的状态,即父进程中已被加锁的互斥锁在子进程中也是被锁住的,这就引起了一个问题:子进程可能不清楚从父进程继承而来的互斥锁的具体状态(是加锁还是解锁状态),这个互斥锁可能被加锁了,但不是由调用fork
的线程锁住的,而是由其他线程锁住的,此时,子进程若再次对该互斥锁加锁会导致死锁,如以下代码所示:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>pthread_mutex_t mutex;/* 子线程运行的函数,它首先获得互斥锁 mutex ,然后暂停5s,再释放该互斥锁 */
void *another(void *arg) {printf("in child thread, lock the mutex\n");pthread_mutex_lock(&mutex);sleep(5);pthread_mutex_unlock(&mutex);
}int main() {pthread_mutex_init(&mutex, NULL);pthread_t id;pthread_create(&id, NULL, another, NULL);/* 父进程中的主线程暂停1s,以确保在执行 fork 前,子线程已经开始运行并获得了互斥量 mutex */sleep(1);int pid = fork();if (pid < 0) {pthread_join(id, NULL);pthread_mutex_destroy(&mutex);return 1;} else if (pid == 0) {printf("I am in the child, want to get the lock\n");/* 子进程从父进程继承了互斥锁 mutex 的状态,该互斥锁处于锁住的状态 *//* 这是由父进程中的子线程执行 pthread_mutex_lock 引起的,因此以下加锁操作会一直阻塞 *//* 尽管从逻辑上来说它是不应该阻塞的 */pthread_mutex_lock(&mutex);printf("I can not run to here, oop...\n");pthread_mutex_unlock(&mutex);exit(0);} else {wait(NULL);}pthread_join(id, NULL);pthread_mutex_destroy(&mutex);return 0;
}
关键点
- 子线程加锁:在主线程(进程)中创建的子线程首先获取互斥锁并休眠5秒。
fork
调用:在子线程获取互斥锁后,主线程休眠1秒以确保子线程锁定互斥锁,然后调用fork
。fork
之后,父进程和子进程都有一个拷贝的互斥锁状态。- 子进程中的锁行为:由于
fork
后子进程继承了互斥锁的状态,如果该锁被锁定,子进程中的互斥锁也将处于锁定状态。不同的是,子进程中并没有线程拥有这个锁(因为锁的拥有者是父进程的一个线程),因此尝试获取这个锁将会导致子进程永久阻塞。
效果:子进程被阻塞。
不过,pthread
提供了一个专门的函数pthread_atfork
,以确保fork
调用后父进程和子进程都拥有一个清楚的锁状态:
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child) (void));
pthread_atfork
函数将建立3个fork
句柄帮助我们清理互斥锁的状态。
prepare
句柄将在fork
函数创建出子进程前被执行,它可以用来锁住父进程中的互斥锁。parent
句柄是fork
函数创建出子进程后,fork
函数返回前,在父进程中被执行,它的作用是释放所有在prepare
句柄中被锁住的互斥锁。child
句柄是在fork
函数返回前,在子进程中执行,它和parent
句柄一样,也是用于释放所有在prepare
句柄中被锁住的互斥锁。
该函数成功时返回0,失败则返回错误码。
要让以上代码正常工作,需要在fork
调用前加上以下代码:
void prepare () {pthread_mutex_lock ( &mutex );
}void infork () {pthread_mutex_unlock ( &mutex );
}pthread_atfork ( prepare, infork, infork );
效果:未发生死锁。
人话翻译一下它这三个函数在干什么
- 第一段代码导致死锁的原因
- 父子进程共享互斥锁状态导致阻塞:
- 在第一段代码中,首先创建了一个互斥锁
mutex
,并在子线程中获取了这个互斥锁(pthread_mutex_lock(&mutex)
),然后线程休眠 5 秒。接着在父进程中执行fork
操作。 - 子进程会继承父进程中互斥锁的状态,此时互斥锁在子进程中仍然是被锁定的状态,因为它是从父进程继承过来的。当子进程试图再次获取已经被锁定的互斥锁(
pthread_mutex_lock(&mutex)
)时,根据互斥锁的默认行为(这里假设是标准互斥锁PTHREAD_MUTEX_NORMAL
),它会被阻塞,等待锁被释放。但是这个锁不会被释放,因为在子进程中没有其他线程可以释放它(子进程中获取锁的代码还没执行成功),从而导致死锁。
- 在第一段代码中,首先创建了一个互斥锁
- 父子进程共享互斥锁状态导致阻塞:
- 第二段代码不会死锁的原因
pthread_atfork
函数的作用:- 在第二段代码中,添加了
pthread_atfork
相关的函数prepare
和infork
。pthread_atfork
函数的作用是在fork
操作前后对互斥锁进行特殊处理,以避免父子进程之间由于互斥锁状态不一致而导致的问题。 - 在
fork
操作之前,prepare
函数会被调用,它会获取互斥锁(pthread_mutex_lock(&mutex)
)。这里需要注意的是,pthread
库对pthread_atfork
中的prepare
操作可能会有特殊处理,即使互斥锁已经被获取(被父进程中的子线程获取),这个操作也不会导致死锁,而是将互斥锁状态稳定在被锁定状态。 - 当
fork
操作完成后,在子进程中infork
函数会被调用,它会释放互斥锁(pthread_mutex_unlock(&mutex)
)。这样就使得子进程从一个互斥锁已经被解锁的状态开始,当子进程后续尝试获取互斥锁(pthread_mutex_lock(&mutex)
)时,就不会像第一段代码那样被阻塞,从而避免了死锁。
- 在第二段代码中,添加了
- 两段代码的主要区别
- 互斥锁状态处理方式:
- 第一段代码没有对
fork
操作前后的互斥锁状态进行特殊处理,子进程继承了父进程中被锁定的互斥锁状态,并且由于再次尝试获取已锁定的互斥锁而导致死锁。 - 第二段代码通过
pthread_atfork
函数及其相关的prepare
和infork
函数,在fork
操作前后对互斥锁状态进行了清理和调整,使得子进程从一个合理的、互斥锁已解锁的状态开始,避免了子进程因继承不适当的互斥锁状态而导致的死锁问题。
- 第一段代码没有对
- 互斥锁状态处理方式:
总结
就是pthread_atfork函数在进入子进程前会获取一下父进程锁的状态,不管有没有被锁咱给它锁上,如果被锁了,我们知道互斥锁被锁了以后再次加锁会导致死锁,可能它函数内部设计避免了这种情况。然后再把咱锁上的通通解锁,再去执行子进程代码。
相当于子进程代码的初始状态就是任何锁都没有被加锁的情况,就避免了死锁的发生。
3.线程和信号
每个线程都能独立设置信号掩码,进程设置信号掩码的函数是sigprocmask
(见信号掩码),但在多线程环境下应使用pthread_sigmask
函数设置信号掩码:
#include <pthread.h>
#include <signal.h>
int pthread_sigmask ( int how, const sigset_t* newmask, sigset_t* oldmask );
pthread_sigmask
函数的参数与sigprocmask
函数的参数完全相同。pthread_sigmask
函数成功时返回0,失败返回错误码。
由于进程中所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。而且,所有线程共享信号处理函数,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一信号设置的信号处理函数。
因此,我们应该定义一个专门的线程来处理所有信号,这可通过以下两个步骤实现:
- 在主线程创建出其他子线程前就调用
pthread_sigmask
来设置好信号掩码,所有新创建的子线程将自动继承这个信号掩码,这样,所有线程都不会响应被屏蔽的信号了。 - 在某个线程中调用以下函数等待信号并处理:
#include <signal.h>
int sigwait ( const sigset_t* set, int* sig );
set
参数指定要等待的信号的集合,我们可以将其指定为在第 1 步中创建的信号掩码,表示在该线程中等待所有被屏蔽的信号。参数sig
指向的整数用于存储该函数返回的信号值。sigwait
成功时返回0,失败则返回错误码。一旦sigwait
函数成功返回,我们就能对收到的信号做处理了,显然,如果我们使用了sigwait
函数,就不应再为信号设置信号处理函数了。
pthread
还提供了pthread_kill
函数,使我们可以把信号发送给指定线程:
#include <signal.h>
int pthread_kill ( pthread_t thread, int sig );
thread
参数指定目标线程。sig
参数指定待发送信号,如果sig
参数为0,则pthread_kill
不发送信号,但它仍会进行错误检查。我们可用此方法检查目标线程是否存在。pthread_kill
函数成功时返回0,失败则返回错误码。
在一个线程中统一处理所有信号
以下代码取自pthread_sigmask
函数的man手册,它展示了如何通过以上两个步骤实现在一个线程中统一处理所有信号:
主线程设置了一个信号掩码来阻塞特定的信号(在这个例子中是SIGQUIT
和SIGUSR1
),然后创建一个专门的线程来处理这些信号。这种模式是处理多线程环境中信号的推荐方式,因为它避免了信号处理和线程执行之间的竞争条件。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>// perror函数根据全局errno值打印其相应的错误信息到标准错误
#define handle_error_en(en, msg) \ do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)/* 在 sig_thread 函数中,线程循环调用 sigwait 来等待信号 */
static void *sig_thread(void *arg) {sigset_t *set = (sigset_t *)arg;int s, sig;for (; ; ) {// 第二步,调用sigwait等待信号s = sigwait(set, &sig);if (s != 0) {handle_error_en(s, "sigwait");}printf("Signal handling thread got signal %d\n", sig);}
}int main(int argc, char *argv[]) {printf("The PID of this process is: %d\n", getpid()); /* 获取进程 PID */pthread_t thread; /* 线程 */sigset_t set; /* 信号集 */int s;/* 第一步,在主线程中设置信号掩码,信号集set被初始化并添加了SIGQUIT和SIGUSR1信号: */sigemptyset(&set);sigaddset(&set, SIGQUIT);sigaddset(&set, SIGUSR1);/* 使用 pthread_sigmask 来阻塞这些信号 */s = pthread_sigmask(SIG_BLOCK, &set, NULL);if (s != 0) {handle_error_en(s, "pthread_sigmask");}/* 创建处理信号的线程 */s = pthread_create(&thread, NULL, &sig_thread, (void *)&set);if (s != 0) {handle_error_en(s, "thread_create");}pause();
}
运行程序,并在另一个终端中使用kill
命令发送SIGQUIT
或SIGUSR1
信号到程序。例如:
kill -SIGQUIT [pid]
kill -SIGUSR1 [pid]