多线程编程
线程概念
Linux
下线程的本质:LWP
(light weight process
) 轻量级的进程,本质仍然是进程(在Linux
环境下)- 进程: 有独立的进程地址空间,有独立的
PCB
- 线程: 有独立的
PCB
,但是没有独立的地址空间(共享) - 区别: 在于是否共享地址空间 独居(进程) 合租(线程)
Linux
下:- 线程: 最小的执行单位
- 进程: 最小的分配资源的单位,可以看成只有一个线程的进程
- 当利用
creat
函数创建线程之后,进程就会退化成线程 - 所以对于并发执行的进程,如果开启更多的线程,那么就会由更多的线程来抢夺
cpu
的执行权利这就使得该进程有更多的机会执行,但是并不是线程越多执行机会越多 - 可以使用
ps -Lf 进程ID
来查看进程的线程号(不是线程ID
),线程号 -->cpu
执行的最小单位
Linux内核线程实现原理
- 注意以下几点:
- 轻量级进程,也有
PCB
,创建线程使用的底层函数和进程一样,都是clone
- 从内核里面看进程和线程都是一样的,都有各自不同的
PCB
,但是PCB
中执行内存资源的三级页表是相同的 - 进程可以蜕变成线程
- 线程可以看成寄存器和栈的集合
- 在
linux
下,线程是最小的执行单位,进程是最小的分配资源的单位
- 轻量级进程,也有
- 实际上,在一个进程中的用户空间中存储的变量并不是直接通过
MMU
映射到真实的物理内存空间,而是首先借助PCB
中的指针,这一个指针指向一个页目录,页目录中的指针指向页表,页表中的指针指向物理页面,物理页面存在着真实的目录内存,由于创建线程的过程底层其实就是调用了clone
方法,所以他的pcb
中的指针和原来的进程的pcb
中的指针一样,所以指向同样一块内存地址空间 - 三级映射: 进程
PCB
--> 页目录(可以看成数组,首地址位于PCB
中) --> 页表 --> 物理页面 ---> 内存单元
线程的共享和非共享
- 线程共享资源:
- 文件描述符号表
- 每一种信号的处理方式
- 当前工作目录
- 用户
ID
和组ID
- 内存地址空间(
.text
./data
.bss
heap
共享库)(没有栈)
- 线程非共享资源:
- 线程
id
- 处理器线程和栈指针(内核栈)
- 独立的栈空间(用户栈空间)
errorno
变量(是一个全局变量)- 信号屏蔽字
- 调度优先级
- 线程
- 优点:
- 提高程序并法性
- 开销比较小
- 数据通信,共享数据方便
- 缺点:
- 库函数,不稳定
- 调试,编写困难,
gdb
不支持 - 对于信号支持不好
Linux
下实现方法导致进程,线程差别不是特别大
线程控制原语
创建线程
pthread_self函数
- 作用: 获取线程
ID
- 头文件:
<pthread.h>
- 函数原型:
pthread_t pthread_self(void);
- 返回值: 返回线程
ID
- 注意线程
ID
用于在一个进程中标记线程,在Linux
本质就是lu
的别名,其他的系统中使用结构体的方式实现,线程ID
是线程内部的识别标志
pthread_create函数
- 作用: 创建新的线程
- 函数原型如下:
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
- 遍历的时候需要链接
pthread
库 - 参数:
thread
: 传出参数,作用就是可以带出线程ID
attr
: 表示设置线程的属性,一般传入NULL
表示传入默认属性start_routine
: 表示需要传入的执行函数(参数和返回值都是泛型类型)arg
: 表示函数的参数
- 演示
demo
:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<errno.h>
// 子线程的回调函数
// 注意回调函数的类型 (void*)(*func)(void*)
void* my_func(void* arg)
{
// 获取进程号和线程ID
printf("thread: pid = %d , tid = %lu \n" , getpid() , pthread_self());
return NULL;
}
int main()
{
// 使用 pthread_self 获取线程 ID
pthread_t tid;
tid = pthread_self();
printf("tid = %lu \n" , tid); // 相当于独享进程空间的线程
printf("pid = %d \n" , getpid());
// 使用 pthread_create创建线程
int ret = pthread_create(&tid , NULL , my_func , NULL);
if(ret != 0){
perror("create a thread failed !!! \n");
exit(1);
}
printf("main: pid: %d , tid: %lu \n" , getpid() , pthread_self());
// 需要让主线程阻塞等待一段时间
sleep(1);
}
-
注意得到的结果中,
main
和pthread
的pid
一样但是tid
不一样 -
由于需要传入的函数必须是
void*(*func)(void*)
类型,所以如果需要传入各种参数那么就需要定义结构体来作为传入参数,函数会被自动调用
循环创建子线程
- 如果使用以下代码循环创建子线程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
// 回调函数
void* print_pthread(void* args)
{
int i = *((int*)args); // void* 类型相当于泛型,可以传递各种类型
printf("pthread %d: pid - %d , tid - %lu \n" , i + 1, getpid() , pthread_self());
return NULL;
}
int main()
{
// 循环创建多个子线程
pthread_t tid;
int ret , i;
for(i = 0 ; i < 5 ; i ++){
ret = pthread_create(&tid , NULL , print_pthread , (void*)&i);
if(ret != 0){
perror("create thread failed !!! \n");
exit(1);
}
}
printf("main: pid - %d , tid - %lu \n" , getpid() , pthread_self());
sleep(1);
}
- 就会发生如下结果:
pthread 4: pid - 88902 , tid - 124829323757248
pthread 5: pid - 88902 , tid - 124829334243008
pthread 5: pid - 88902 , tid - 124829313271488
pthread 5: pid - 88902 , tid - 124829302785728
main: pid - 88902 , tid - 124829340292928
pthread 6: pid - 88902 , tid - 124829292299968
- 错误原因分析: 以上代码中,由于
main
函数和不同的线程有不同的栈帧,main
函数的栈帧中存在变量i
,如果使用地址传递的方式传递参数,就会导致此时线程的栈中的变量指向main
函数的栈中的变量,但是main
函数中的变量在不断变化,所以就会造成以上结果 - 所以最好在创建子线程的时候,使用值拷贝的方式传递参数
- 这里的
void*
尽管可以当成一个可以转换为任意数据类型的泛型(类似于go
中的空接口类型),void*
占用8
个字节,int
占用4
个字节所以转换的时候不会造成精度的缺失 - 各种类型占用的空间如下,注意指针占用
8
个字节(64
位编译器): - 这里解释以下为什么说
64
位操作系统中int
占用8
个字节:- 注意这里的
int
并不是指的就是int
,而是int
类型的变量,比如long
等,long
在32
位操作系统中占用4
个字节但是在64
位操作系统中占用8
个字节,位数不同的操作系统的寻址能力不同,体现与指针的位数,比如64
位操作系统中的寻址范围就是 $2^64$ 所以指针就占用8
个字节也就是64
个bit
- 注意这里的
- 正确代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
// 回调函数
void* print_pthread(void* args)
{
int i = (int)args; // void* 类型相当于泛型,可以传递各种类型
printf("pthread %d: pid - %d , tid - %lu \n" , i + 1, getpid() , pthread_self());
return NULL;
}
int main()
{
// 循环创建多个子线程
pthread_t tid;
int ret , i;
for(i = 0 ; i < 5 ; i ++){
ret = pthread_create(&tid , NULL , print_pthread , (void*)i);
if(ret != 0){
perror("create thread failed !!! \n");
exit(1);
}
}
printf("main: pid - %d , tid - %lu \n" , getpid() , pthread_self());
sleep(1);
}
线程和共享
- 线程之间共享全局变量
- 线程默认共享数据段,代码段的呢个地址空间,常用的就是全局变量,但是进程不会共享全局变量,只可以借助
mmap
(进程中遵循读时共享,写时复制的原则,其实就是建立了副本)
- 线程默认共享数据段,代码段的呢个地址空间,常用的就是全局变量,但是进程不会共享全局变量,只可以借助
- 注意共享的含义就是子线程改变变量,父线程中的数据也会进行相同的改变
- 验证:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int var = 20;
void* pthread_handler(void* arg)
{
var = 200;
printf("thread: var = %d \n" , var);
return NULL;
}
int main()
{
pthread_t tid ;
int ret;
ret = pthread_create(&tid , NULL , pthread_handler , NULL);
if(ret != 0){
perror("creat thread failed !!! \n");
exit(1);
}
sleep(1);
printf("main: var = %d \n" , var);
}
- 注意
C
语言中各个内存区域和作用:
pthread_exit函数
- 作用: 线程退出
- 函数原型:
[[noreturn]] void pthread_exit(void *retval);
- 参数:
retval
表示传出参数,用于承载子线程中返回值 - 为什么使用
pthread_exit
,这是由于exit
用于退出整个进程,而不是退出线程,return
表示返回给函数调用者 - 利用
pthread_exit
退出只是将线程退出,并且不会影响其他进程 - 各种退出效果总结如下:
return
返回到调用者那里去pthread_exit
将调用该函数的线程退出exit
退出它的进程
- 三者的对比如下:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
// 演示 return exit pthread_exit 三者之间的区别
void* pthread_handler(void* arg)
{
int i = (int)arg;
if(i == 2){
// exit(0); // 表示退出当前进程
// return NULL; 返回给函数的调用这
pthread_exit(NULL); // 表示退出线程
}
printf("The %d th thread , tid: %ld \n" , i + 1 , pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret;
int i;
for(i = 0 ; i < 5 ; i ++){
ret = pthread_create(&tid , NULL , pthread_handler , (void*)i);
if(ret != 0){
perror("create thread failed !!! \n");
exit(1);
}
}
// sleep(1);
pthread_exit(NULL); // 表示退出父进程
}
pthread_join函数
- 作用: 阻塞等待线程退出,获取线程退出状态,其作用就是对应于进程中的
waitpid()
函数 - 函数原型:
int pthread_join(pthread_t thread, void **retval);
- 参数:
thread
表示需要回收的线程ID
retval
表示获取函数的退出状态(需要回收void*
)(比如进程的退出值就是pid
)(注意这里的设计逻辑,如果返回值是int
类型,那么就需要使用int*
类型回收返回值,如果返回值是void*
类型,那么就需要使用void**
回收返回值)(参考wait
函数使用&status
作为传出参数)
- 注意
pthread_join
会阻塞等待 - 另外一个小的知识点,注意指针只有分配了内存空间才可以使用常量赋值,但是如果没有分配内存空间还是可以使用指针或者地址赋值
char* p = NULL;
p = "123";// error
char* p = (char*)malloc(sizeof(char) * 10);
p = "hello"; //正确
int* p;
int k = 1;
p = &k // 正确
pthread_join
使用方式如下:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
// 用于传递的数据
struct thrd{
int var;
char name[256];
};
void* thread_handler(void* arg)
{
struct thrd* ret_val;
ret_val = (struct thrd*) malloc (sizeof(struct thrd));
ret_val -> var = 100;
// 注意常量无法使用 = 进行赋值操作
strcpy(ret_val -> name , "hello thread!!!");
return (void*)ret_val;
}
int main()
{
pthread_t tid;
int ret;
ret = pthread_create(&tid , NULL , thread_handler , NULL);
if(ret != 0){
perror("create thread failed !!! ");
exit(1);
}
// 进行回收
struct thrd* res;
ret = pthread_join(tid , (void**)&res); // 注意参数
if(ret != 0){
perror("resouce my child failed !!!");
exit(0);
}
// 打印数据
printf("var = %d , name = %s \n" , res -> var , res -> name);
pthread_exit(NULL);
}
- 需要注意的事项如下:
- 在线程执行的函数中,不要返回一个局部变量的地址(此时这个函数的栈帧已经被销毁了,返回一个没有意义的栈地址)
- 可以在
main
函数中定义一个变量,之后在线程的执行函数中操作这一个变量,这是由于函数执行完了之后main
函数的栈地址仍然存在依然可用
- 以下情况也正确但是还是最好在堆区开启空间:
- 可以返回局部变量的值,但是不可以返回局部变量的地址
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
// 用于传递的数据
struct thrd{
int var;
char name[256];
};
void* thread_handler(void* arg)
{
struct thrd* ret_val = (struct thrd*) arg;
// ret_val = (struct thrd*) malloc (sizeof(struct thrd));
ret_val -> var = 100;
// 注意常量无法使用 = 进行赋值操作
strcpy(ret_val -> name , "hello thread!!!");
return (void*)ret_val;
}
// void* thread_handler(void* arg)
// {
// return (void*)100;
// }
int main()
{
pthread_t tid;
int ret;
struct thrd* arg;
ret = pthread_create(&tid , NULL , thread_handler , (void*)arg);
if(ret != 0){
perror("create thread failed !!! ");
exit(1);
}
// 进行回收
struct thrd* res;
// int res;
ret = pthread_join(tid , (void**)&res); // 注意参数
if(ret != 0){
perror("resouce my child failed !!!");
exit(0);
}
// 打印数据
printf("var = %d , name = %s \n" , res -> var , res -> name);
printf("var = %d , name = %s \n" , arg -> var , arg -> name);
// printf("var = %d \n" , res);
pthread_exit(NULL);
}
- 连续: 循环创建多个子线程并且回收:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void* thread_handler(void* arg)
{
int i = (int)arg;
printf("I am the %d th child , my tid is %ld \n" , i + 1, pthread_self());
return NULL;
}
int main()
{
// 循环创建多个子线程
pthread_t tid[5];
int ret;
for(int i = 0 ; i < 5 ; i ++){
ret = pthread_create(&tid[i] , NULL , thread_handler , (void*)i);
if(ret != 0){
perror("create thread failed !!! ");
exit(1);
}
}
// 循环退出
for(int i = 0 ; i < 5 ; i ++){
ret = pthread_join(tid[i] , NULL);
printf("Successfully resource my child: %d \n" , i + 1);
}
pthread_exit(NULL);
}
pthread_cancel函数
- 作用: 用于杀死线程
- 函数原型如下:
int pthread_cancel(pthread_t thread);
- 参数:
thread
表示需要杀死的进程ID
- 注意利用
pthread_cancel
把进程杀死的时候 - 注意利用
pthread_cancel
杀死进程的时候进入内核,需要进入内核的契机,如果子进程一直执行就没有取消点了,如果没有保存点,那么就可以使用pthread_testcancel()
来设置取消点 - 成功被
pthread_cancel
杀死的线程,返回-1
,可以使用pthread_join
回收这一个值 - 演示
demo
如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void* thread_handler(void* arg)
{
while(1){
// printf("I am the child , tid : %ld \n" , pthread_self());
// sleep(1);
pthread_testcancel();
}
return (void*)100;
}
int main()
{
// 利用 pthread_cancel 杀死线程
pthread_t tid;
int ret = pthread_create(&tid , NULL , thread_handler , NULL);
sleep(5);
// 杀死线程
ret = pthread_cancel(tid);
if(ret != 0){
perror("can not canel this thread !!!");
exit(1);
}
// 进行线程的回收
int res;
ret = pthread_join(tid , (void**)&res);
if(ret != 0){
perror("resource thread failed !!!");
exit(1);
}
printf("exit code is %d \n" , res);
}
pthread_detach函数
- 作用: 实现线程分离,让线程脱离与主线程而存在
- 函数原型如下:
int pthread_detach(pthread_t thread);
- 参数:
- 线程号
- 对于线程中出现的错误,不可以使用
perror
进行打印,这是由于无法翻译错误条件,可以使用strerror(errno)
结合fprintf
进行错误处理即可 - 注意即检测出错返回的方式 !!!
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
// 创建子线程任务
void* thread_handler(void* arg)
{
while(1){
printf("[thread] pid : %ld \n" , pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
// 创建
pthread_t tid;
int ret = pthread_create(&tid , NULL , thread_handler , NULL);
if(ret == -1){
// 注意错误处理方式
fprintf(stderr , "create thread failed: %s \n" , strerror(ret));
exit(1);
}
sleep(1);
ret = pthread_detach(tid);
if(ret != 0){
fprintf(stderr , "detach thread failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_join(tid , NULL);
if(ret != 0){
// perror("resource thread failed !!!");
fprintf(stderr , "resource thread failed: %s \n" , strerror(ret));
exit(1);
}
}
进程控制原语和线程控制原语的区别
- 创建:
fork
pthread_create
- 回收:
wait waitpid
pthread_join
- 杀死:
kill
pthread_cancel
- 获取信息:
getpid()
pthread_self
- 退出:
pthread_exit
exit
线程属性设置分离线程
- 线程属性就是创建线程时候的第二个参数
- 早期的
Linux kernel
中的线程状态结构体:
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_attr_destory
销毁线程属性所占用的资源- 注意这两个函数的作用就是操作线程属性而不是操作线程
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
分离状态以及非分离状态
- 非分离状态: 线程默认的属性就是非分离状态,这一种情况下,原有的线程等待创建的线程结束,只有当
pthread_join
函数返回的时候,创建的线程才算终止,才可以释放自己占用的系统资源 - 分离状态: 分离线程没有被其他的线程等待,自己运行结束了,线程也终止了,马上释放系统资源,应该根据自己的需要,选择适当的分离状态
- 设置线程分离的函数:
pthread_attr_setdetachstate
- 查看线程分离状态的函数:
pthread_attr_getdetachstate
- 参数(detachstate):
PTHREAD_CREATE_DETACHED
(分离线程)PTHREAD_CREATE_JOINABLE
(非分离线程)
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr,
int *detachstate);
- 演示
demo
如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
void* thread_handler(void* arg)
{
while(1){
printf("[thread] pid : %ld \n " , pthread_self());
sleep(1);
}
return (void*)100;
}
int main()
{
pthread_attr_t attr;
int ret = pthread_attr_init(&attr);
if(ret != 0){
fprintf(stderr , "init attr failed: %s \n" , strerror(ret));
exit(1);
}
// 设置分离
ret = pthread_attr_setdetachstate(&attr , PTHREAD_CREATE_DETACHED);
if(ret != 0){
fprintf(stderr , "detach thread failed: %s \n" , strerror(ret));
exit(1);
}
// 查看
int detachstate;
pthread_attr_getdetachstate(&attr , &detachstate);
if(detachstate == PTHREAD_CREATE_DETACHED) {
printf("Successfully set status !!! \n");
}
// 创建线程
pthread_t tid;
ret = pthread_create(&tid , &attr , thread_handler , NULL);
if(ret != 0){
fprintf(stderr , "create thread failed: %s \n" , strerror(ret));
exit(1);
}
// 回收线程
ret = pthread_join(tid , NULL);
if(ret != 0){
fprintf(stderr , "resource thread failed : %s \n" , strerror(ret));
exit(1);
}
ret = pthread_attr_destroy(&attr);
if(ret != 0){
fprintf(stderr , "destroy attr failed : %s \n" , strerror(ret));
exit(1);
}
}
- 总结:
- 定义线程属性
- 初始化线程属性
- 设置线程属性为分离状态
- 借助修改之后的线程属性来创建分离态的线程
- 回收看是否分离成功
线程的使用注意事项
- 主线程退出其他线程不退出,主线程需要调用
pthread_exit
方法 - 避免僵尸线程:
pthread_join
pthread_detach
pthread_create
指定分离属性,被join
线程会受到线程在回收之前可能就释放完了自己的所有内存资源,所以不应当返回被回收线程栈中的值 malloc
和mmap
申请的内存可以被其他线程释放(共享堆区)- 需要避免在多线程模型中调用
fork
,除非马上exec
,子进程只有调用fork
的进程存在,其他进程在子进程中都需要使用pthread_exit
- 信号的复杂语义很难和多线程共存,应该避免在多线程中引入信号机制
线程同步
- 线程同步,指的就是一个线程发生功能调用的瞬间,在没有得到结果之前,该调用不用返回 同时其他线程为保证数据一致性,不能调用该功能
- 多个线程同时操作一个共享变量的时候就需要进行进程同步操作(比如取钱的时候,如果一个线程作了判断之后另外的线程也进来对于数据进行操作,那么就会导致两个线程都对于这一个数据进行了操作,但还是以为只有一个变量对于数据进行了操作导致出错)
- 数据混乱的原因:
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程之间缺乏必要的同步机制
利用互斥锁进行线程同步
互斥量(mutex)
- 也就是互斥锁,作用就是利用互斥锁锁住全局变量,那么就可以保证公共资源每一次只会被一个线程进行操作,但是如果某一个线程直接访问全局变量那还是会导致数据不同布的问题,所以这些解决线程同步问题使用的锁都是建议锁而不是强制锁
- 锁的使用:
- 建议锁,用于锁住全局变量,锁住全局变量之后,就只有一个线程可以操作全局变量
操作函数
- 需要使用的函数如下:
pthread_mutex_init 函数
pthread_mutex_destory 函数
pthread_mutex_lock 函数
pthread_mutex_trylock 函数
pthread_mutex_unlock 函数
- 以上几个函数的返回值都是: 成功返回
0
, 失败返回错误号 - 关注以下几个类型:
pthread_mutex_t 类型 本质是一个结构体,为了简化理解,应用的时候可以忽略实现细节,简单当成整数看待
pthread_mutex_t mutex; 变量 mutext只有两种取值: `0` 和 `1`
- 使用锁的步骤:
pthread_mutex_t lock
创建锁pthread_mutex_init
初始化pthread_mutex_lock
加锁- 操作全局变量
pthread_mutex_unlock
解锁pthread_mutex_destroy
销毁锁
pthread_mutex_init函数
- 作用: 初始化锁
- 函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
- 参数:
mutex
表示锁attr
表示锁相关的配置
- 成功返回
0
失败返回errorno
restrict
关键字用于限定指针变量,被该关键字限定的指针变量所指向的内存操作,必须由本指针完成
pthread_mutex_destory函数
- 作用: 销毁锁
- 函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数:
- 表示需要释放的锁
pthread_mutex_lock函数
- 作用: 加锁
- 函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数:
- 表示所加上的锁
- 成功返回
0
,失败返回errorno
pthread_mutex_unlock函数
- 作用: 解锁
- 函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数: ...
- 返回值: ...
- 进行互斥锁操作的
demo
如下: - 这里操作的全局变量就是
stdout
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
// 定义锁
pthread_mutex_t mutex;
// 子线程需要执行的操作
void* thread_handler(void* arg)
{
while(1){
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand() % 3);
printf("world \n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 随机时间种子
srand(time(NULL));
// 初始化锁
pthread_t tid;
int ret;
ret = pthread_mutex_init(&mutex , NULL);
if(ret != 0){
fprintf(stderr , "init mutex failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_create(&tid , NULL , thread_handler , NULL);
if(ret != 0){
fprintf(stderr , "create thread failed: %s \n" , strerror(ret));
exit(1);
}
sleep(3);
while(1){
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD \n");
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
ret = pthread_mutex_destroy(&mutex);
if(ret != 0){
fprintf(stderr , "destory mutex failed: %s \n" , strerror(ret));
exit(1);
}
}
- 使用技巧: 注意
mutex
的位置,不要再锁操作的代码中进行休眠,否则很容易导致某一个线程不断执行某一个业务逻辑,长时间占用CPU
- 一定需要注意锁的粒度,越小越好(访问共享数据之前加锁,访问结束之后立刻解锁)
mutex
类型可以看成int
类型,初始化之后可以看作mutex = 1
lock
可以想象成mutex --
同时unlock
可以想象成mutex++
,虽然本质就是结构体,但是这样利于学习- 对于
mutex
的操作:- 加锁:
--
操作,阻塞线程 - 解锁:
++
操作,唤醒阻塞在锁上的线程 try
锁: 尝试加锁,成功++
,失败返回(注意此时不会阻塞,设置错误号为EBUSY
)
- 加锁:
try
锁使用pthread_mutex_trylock
函数
死锁
- 不是一种锁,而是一种现象,是使用锁不恰当而导致的错误
- 会产生死锁的现象:
- 对于一个锁反复
lock
(自己把自己锁住了) - 两个线程各自持有一把锁,都在请求另外一把锁
- 对于一个锁反复
- 两种现象的解释如下:
- 第一种情况的死锁:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
pthread_mutex_t mutex; // 模拟死锁
int var = 1;
void* thread_handler(void* arg)
{
while(var <= 100){
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
printf("[thread] tid = %ld , var = %d \n" , pthread_self() , var);
var ++;
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret;
ret = pthread_mutex_init(&mutex , NULL);
if(ret != 0){
fprintf(stderr , "init mutex error: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_create(&tid , NULL , thread_handler , NULL);
if(ret != 0){
fprintf(stderr , "create mutex error: %s \n" , strerror(ret));
exit(1);
}
// 阻塞回收
ret = pthread_join(tid , NULL);
if(ret != 0){
fprintf(stderr , "join mutex error: %s \n" , strerror(ret));
exit(1);
}
// 销毁
ret = pthread_mutex_destroy(&mutex);
if(ret != 0){
fprintf(stderr , "destory mutex error: %s \n" , strerror(ret));
exit(1);
}
}
- 第二种情况的死锁:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
int var1 = 1;
int var2 = 1;
void* thread_handler1(void* arg)
{
while(var1 <= 100){
pthread_mutex_lock(&mutex_a);
pthread_mutex_lock(&mutex_b);
printf("[thread] tid = %ld , var1 = %d \n " , pthread_self() , var1);
var1 ++;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
}
return NULL;
}
void* thread_handler2(void* arg)
{
while(var2 <= 100){
pthread_mutex_lock(&mutex_b);
pthread_mutex_lock(&mutex_a);
printf("[thread] tid = %ld , var1 = %d \n " , pthread_self() , var2);
var2 ++;
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
}
return NULL;
}
int main()
{
pthread_t tid[2];
int ret;
ret = pthread_mutex_init(&mutex_a , NULL);
if(ret != 0){
fprintf(stderr , "init mutex_a failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_mutex_init(&mutex_b , NULL);
if(ret != 0){
fprintf(stderr , "init mutex_b failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_create(&tid[0] , NULL , thread_handler1 , NULL);
if(ret != 0){
fprintf(stderr , "create thread_a failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_create(&tid[1] , NULL , thread_handler2 , NULL);
if(ret != 0){
fprintf(stderr , "create thread_b failed: %s \n" , strerror(ret));
exit(1);
}
// 回收
for(int i = 0 ; i < 2 ; i ++){
ret = pthread_join(tid[i] , NULL);
if(ret != 0){
fprintf(stderr , " join thread failed: %s \n" , strerror(ret));
exit(1);
}
}
// 销毁
ret = pthread_mutex_destroy(&mutex_a);
if(ret != 0){
fprintf(stderr , "destory thread_a failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_mutex_destroy(&mutex_b);
if(ret != 0){
fprintf(stderr , "destory thread_b failed: %s \n" , strerror(ret));
exit(1);
}
}
读写锁
- 与互斥锁类似,但是读写锁允许更高的并型性,他的特性为: 写独占,读共享
- 注意特点:
- 读共享,写独占
- 写锁优先级高
- 只有一把锁
- 使用读的方式给数据加锁--读锁,以写的方式给数据加锁--写锁
- 注意如果一个线程已经拿到锁了,就算是写锁的线程也会被阻塞,拿不到锁
- 这里思考以下为什么写锁的优先级别高,这是由于只有在写锁之后,读取到的数据才是真正的数据,避免了"读未提交的问题"
- 这里介绍几种情况,
Tn
表示每一个线程n
表示线程的顺序,n
越小表示顺序越靠前:- 如果
T1
为r
T2
为w
那么T2
就会获取锁 - 如果
T1
为r
并且已经获取到锁了,如果T2
为w
,那么T2
还是会被阻塞,这是由于此时锁已经被获取了 - 如果
T1
为r
并且获取了锁 ,T2
为r
T3
为w
由于此时T3
和T2
都没有获取到锁,所以此时更具写锁优先级别高的元素应该是T3
首先拿到锁,之后T2
拿到进行数据的读取
- 如果
读写锁特性
- 读写锁是 "写模式加锁" 时,解锁之前,所有对该解锁加锁的线程都会被阻塞
- 读写锁是"读模式加锁" 时,如果线程使用读模式对其加锁成功,如果线程以写模式则会阻塞
- 读写锁时 "读模式加锁" 时,既有试图使用写模式加锁的线程,也有试图使用读模式加锁的线程,那么读写锁会阻塞随后的读模式锁请求,优先满足写模式锁,读锁写锁并行阻塞,但是写锁的优先级别高于读锁
- 读写锁也叫做共享-独占锁,当读写锁使用读模式锁住的时候,它是使用共享模式锁住的,当它使用写模式锁住的时候,它是使用独占模式锁住的,写独占,读共享
- 读写锁非常适合于对于数据结构的读的次数远大于写的次数
读写锁的常用函数
pthread_rwlock_init
pthread_rwlock_destory
pthread_rwlock_wrlock
pthread_rwlock_rdock
pthread_rwlock_trywrlock
pthread_rwlock_tryrdlock
pthread_rwlock_unlock
- 以上的几个函数成功返回
0
失败返回可以通过strerror()
判断 - 锁的类型如下:
pthread_rwlock_t
类型,用于定义一个读写锁变量pthread_rwlock_t rwlock
- 读写锁在读的线程多的时候效率会高于互斥锁
pthread_rwlock_init函数
- 作用: 初始化读写锁
- 函数原型:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
- 参数参考
pthread_lock_init
pthread_rwlock_destory函数
- 作用: 销毁读写锁
- 函数原型:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
- 注意其他函数基本可以参考互斥锁的相关的
API
- 注意三句话即可:
- 读共享,写独占
- 写锁优先级高
- 全局只有一把读写锁
- 读写锁的
demo
演示:
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
// 读锁
int counter = 1;
pthread_rwlock_t rwlock; // 表示全局的读写锁
void* read_handler(void* arg)
{
int i = (int)arg;
while(1){
pthread_rwlock_rdlock(&rwlock);
printf("---------------read %d , tid = %lu , counter = %d \n" , i , pthread_self() , counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
void* write_handler(void* arg)
{
int i = (int)arg;
int t;
while(1){
pthread_rwlock_wrlock(&rwlock);
usleep(1000);
t = counter;
printf("---------------write %d , tid = %lu , t = %d , counter = %d \n" , i , pthread_self() , t , ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(5000);
}
return NULL;
}
int main()
{
// 初始化
pthread_rwlock_init(&rwlock , NULL);
pthread_t tid[8];
// 创建线程
int ret;
for(int i = 0 ; i < 3 ; i ++){
ret = pthread_create(&tid[i] , NULL , write_handler , (void*)i);
if(ret != 0){
fprintf(stderr , "create write thread failed: %s \n" , strerror(ret));
exit(1);
}
}
for(int j = 3 ; j < 8 ; j ++){
ret = pthread_create(&tid[j] , NULL , read_handler , (void*)j);
if(ret != 0){
fprintf(stderr , "create read lock failed: %s \n" , strerror(ret));
exit(1);
}
}
// 开始循环回收
for(int k = 0 ; k < 8 ; k ++){
pthread_join(tid[k] , NULL);
}
// 销毁锁
pthread_rwlock_destroy(&rwlock);
}
- 互斥锁:
pthread_mutex_t
- 读写锁:
pthread_rwlock_t
条件变量
- 条件变量本身不是锁,但是它也可以造成线程阻塞,通常情况下与互斥锁配合使用,给多线程提供一个会合的场所
主要的应用函数
pthread_cond_init
pthread_cond_destroy
pthread_cond_wait
(相当于条件满足)pthread_cond_timewait
(等待条件满足相当于try
锁)pthread_cond_signal
(表示条件满足进行通知)pthread_cond_broadcast
(表示通知的时候使用广播模式)- 以上六个函数的返回值都是成功返回
0
, 失败直接返回错误号 - 常用类型:
pthread_cond_t
类型,用于定义条件变量pthread_cond_t cond
pthread_cond_init函数
- 作用: 初始化一个条件变量
- 注意条件判断
- 函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 参数和返回值不用多说,此时可以说一下后面的静态初始化方式,其实就是利用宏定义来初始化这一个锁,基本上和
pthread_mutex_init
和pthread_rwlock_init
类似
pthread_cond_wait函数
- 作用:
- 阻塞等待条件变量
cond
满足 - 释放已经掌握的互斥锁(解锁互斥量),相当于
pthread_mutex_unlock
(注意1
和2
是一个原子操作) - 当被唤醒,
pthread_cond_wait
函数返回的时候,解除阻塞并且重新申请互斥锁(pthread_mutex_lock
)
- 阻塞等待条件变量
- 函数原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
pthread_cond_wait
工作原理:
条件变量的生产者消费者模型
- 生产者消费者模型图如下:
- 公共操作:
- 首先定义锁
pthread_mutex_t lock
- 初始化锁
pthread_mutex_init(&lock , NULL)
- 首先定义锁
- 消费者:
- 加锁(尝试获取数据):
pthread_mutex_lock(&lock)
- 条件是否满足,不满足阻塞等待:
pthread_cond_wait(&cond , &lock)
- 访问共享数据
- 释放锁
pthread_mutex_unlock(&lock)
,并且循环上述操作
- 加锁(尝试获取数据):
- 生产者:
- 生产数据
- 尝试获取锁(加锁):
pthread_mutex_lock(&lock)
- 将数据存放到公共区域
- 解锁
pthread_mutex_unlock(&lock)
- 唤醒消费者,满足条件:
pthread_cond_signal(&cond)
或者pthread_cond_broadcast(cond)
- 循环生产后续数据
- 消费者生产者模型代码实现:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
typedef struct msg{
int num;
struct msg* next;
}Msg; // 共享数据相当于链表
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t cond; // 条件变量
Msg* pub_msg; // 表示公共的数据
int i; // 表示消息的编号
// 消费者
void* consumer_handler(void* arg)
{
while(1){
// 1. 首先尝试获取数据,加锁
pthread_mutex_lock(&mutex);
// 2. 判断条件变量是否满足
while(pub_msg -> next == NULL){ // 注意条件判断,如果公共数据区存在数据的,那么就可以直接取出数据
pthread_cond_wait(&cond , &mutex);
}
// 3. 访问共享数据,此时已经加锁了
// 利用头删法消费数据
Msg* temp = pub_msg -> next;
pub_msg -> next = temp -> next;
printf("消费者获取: Message-%d \n" , temp -> num);
free(temp);
// 4. 释放锁
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
// 生产者
void* producer_handler(void* arg)
{
while(1){
// 1. 首先生产数据
Msg* node = (Msg*) malloc (sizeof(Msg));
sleep(1);
node -> num = i;
i ++;
// 2. 尝试加锁
pthread_mutex_lock(&mutex);
// 3. 把数据放入到公共区域
node -> next = pub_msg -> next;
pub_msg -> next = node;
printf("生产者生产: Message-%d \n" , node -> num);
// 4. 解锁
pthread_mutex_unlock(&mutex);
// 5. 唤醒消费者
pthread_cond_signal(&cond);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化
srand(time(NULL));
i = 1;
pthread_mutex_init(&mutex , NULL);
pthread_cond_init(&cond , NULL);
pub_msg = (Msg*) malloc (sizeof(Msg));
// 创建线程
int ret;
pthread_t consumer_tid , producer_tid;
ret = pthread_create(&consumer_tid , NULL , consumer_handler , NULL);
if(ret != 0){
fprintf(stderr , "create consumer failed: %s \n" , strerror(ret));
exit(1);
}
ret = pthread_create(&producer_tid , NULL , producer_handler , NULL);
if(ret != 0){
fprintf(stderr , "create producer failed: %s \n" , strerror(ret));
exit(1);
}
// 回收
pthread_join(consumer_tid , NULL);
pthread_join(producer_tid , NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
多个消费者使用while做
- 分析一下多个消费者消费时候的流程: 当某一个线程被唤醒的时候,由与使用的都是同样一个
cond
所以都会被唤醒,所以如果被唤醒之后即可结束(使用if
)判断条件的情况下,这一个线程就会立刻阻塞到lock
的位置导致缺少对于公共数据是否为空减少判断导致出错 - 改进代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
typedef struct msg{
int num;
struct msg* next;
}Msg; // 共享数据相当于链表
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t cond; // 条件变量
Msg* pub_msg; // 表示公共的数据
int i; // 表示消息的编号
// 消费者
void* consumer_handler(void* arg)
{
int v = (int)arg;
while(1){
// 1. 首先尝试获取数据,加锁
pthread_mutex_lock(&mutex);
// 2. 判断条件变量是否满足
while(pub_msg -> next == NULL){ // 注意条件判断,如果公共数据区存在数据的,那么就可以直接取出数据
pthread_cond_wait(&cond , &mutex);
}
// 3. 访问共享数据,此时已经加锁了
// 利用头删法消费数据
Msg* temp = pub_msg -> next;
pub_msg -> next = temp -> next;
printf("消费者:%d 获取: Message-%d \n" , v + 1, temp -> num);
free(temp);
// 4. 释放锁
pthread_mutex_unlock(&mutex);
sleep(2);
}
return NULL;
}
// 生产者
void* producer_handler(void* arg)
{
while(1){
// 1. 首先生产数据
Msg* node = (Msg*) malloc (sizeof(Msg));
sleep(1);
node -> num = i;
i ++;
// 2. 尝试加锁
pthread_mutex_lock(&mutex);
// 3. 把数据放入到公共区域
node -> next = pub_msg -> next;
pub_msg -> next = node;
printf("生产者生产: Message-%d \n" , node -> num);
// 4. 解锁
pthread_mutex_unlock(&mutex);
// 5. 唤醒消费者
pthread_cond_signal(&cond);
sleep(0.5);
}
return NULL;
}
int main()
{
// 初始化
srand(time(NULL));
i = 1;
pthread_mutex_init(&mutex , NULL);
pthread_cond_init(&cond , NULL);
pub_msg = (Msg*) malloc (sizeof(Msg));
// 创建线程
int ret;
pthread_t producer_tid;
pthread_t consumer_tid[2];
for(int j = 0 ; j < 2 ; j ++){
ret = pthread_create(&consumer_tid[j] , NULL , consumer_handler , (void*)j);
if(ret != 0){
fprintf(stderr , "create consumer failed: %s \n" , strerror(ret));
exit(1);
}
}
ret = pthread_create(&producer_tid , NULL , producer_handler , NULL);
if(ret != 0){
fprintf(stderr , "create producer failed: %s \n" , strerror(ret));
exit(1);
}
// 回收
for(int j = 0 ; j < 2 ; j ++){
pthread_join(consumer_tid[j] , NULL);
}
pthread_join(producer_tid , NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
条件变量 signal注意事项
pthread_cond_signal()
: 唤醒阻塞在条件变量上的(至少)一个线程pthread_cond_broadcast()
: 唤醒阻塞在条件变量上的所有线程
信号量
- 相当于初始化值为
N
的互斥量,可以当成锁看待(这样允许有N
个线程同时操作共享变量,提高了执行效率)(N
表示可以同时执行对于共享变量操作的线程个数)
主要应用函数
sem_init
sem_destroy
sem_wait
sem_trywait
(相当于pthread_mutex_trylock
)sem_timedwait
sem_post
- 以上函数的返回值都是成功返回
0
,失败返回-1
,同时设置error
(注意没有pthread
前缀) - 使用的信号量类型:
sem_t
类型: 本质仍然是结构体,但是应用期间可以看成简单函数,忽略实现细节(类似于文件描述符)sem_t sem
规定信号量sem
不可以< 0
- 头文件:
<semaphore.h>
信号量操作函数
sem_wait
:- 信号量大于
0
,则信号量--
- 信号量等于
0
,则造成信号阻塞 - 对应于
pthread_mutex_lock
- 信号量大于
sem_post
:- 将信号量
++
,同时唤醒阻塞在信号量上面的线程(类比pthread_mutex_unlock
)
- 将信号量
- 但是,由于
sem_t
的实现对于用户隐藏,所以所谓的操作++
或者--
都是只可以通过函数实现,不可以直接通过++
,--
符号 - 信号量的初始值决定了占用信号量的线程个数
- 信号量: 可以应用于线程或者进程
sem_init函数
- 作用: 初始化信号量
- 函数原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数:
sem
信号量pshared
0
表示线程之间同步,1
表示进程之间同步value
就是表示 信号量的N
其他函数
sem_timedwait
: 指定阻塞时间(尝试时间)- 函数原型如下:
int sem_timedwait(sem_t *restrict sem,
const struct timespec *restrict abs_timeout);
- 注意第二个参数:
abs_timeout
就是指的绝对时间相当于1970.01.01
利用信号量实现生产者消费者模型
- 利用信号量实现生产者消费者模型:
- 注意实现方式中可以把
sem_wait
当成对于信号量的--
操作,sem_post
看成++
操作 - 代码实现如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
#include<semaphore.h>
#define NUM 5 // 表示队列的最大长度
int queue[NUM]; // 表示队列
sem_t star_num; // 表示 物品的数量
sem_t blank_num; // 表示空格的数量
// 消费者
void* consumer_handler(void* arg)
{
int i = 0; // 表示开始下标
while(1){
// 1. 首先利用 star_num 锁住
sem_wait(&star_num);
// 2. 消费元素
int target = queue[i];
printf("-----consumer: %d \n" , target);
queue[i] = 0;
// 3. 唤醒生产者
sem_post(&blank_num);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
return NULL;
}
// 生产者
void* producer_handler(void* arg)
{
int i = 0;
while(1){
// 1. 首先利用blank_num锁住
sem_wait(&blank_num);
queue[i] = rand() % 1000 + 1;
printf("-----producer: %d \n" , queue[i]);
// 2. 表示唤醒消费者
sem_post(&star_num);
i = (i + 1) % NUM;
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化信号量
srand(time(NULL));
for(int i = 0 ; i < NUM ; i ++){
queue[i] = 0;
}
sem_init(&star_num , 0 , 0);
sem_init(&blank_num , 0 , NUM);
// 创建线程
pthread_t consumer_tid , producer_tid;
int ret;
ret = pthread_create(&consumer_tid , NULL , consumer_handler , NULL);
if(ret != 0){
fprintf(stderr , "create consumer failed : %s \n" , strerror(ret));
exit(1);
}
ret = pthread_create(&producer_tid , NULL , producer_handler , NULL);
if(ret != 0){
fprintf(stderr , "create producer failed : %s \n" , strerror(ret));
exit(1);
}
// 回收
pthread_join(consumer_tid , NULL);
pthread_join(producer_tid , NULL);
// 销毁
sem_destroy(&star_num);
sem_destroy(&blank_num);
}