欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 产业 > 【Linux】多线程(1)

【Linux】多线程(1)

2025/5/22 8:05:06 来源:https://blog.csdn.net/weixin_74792326/article/details/145903033  浏览:    关键词:【Linux】多线程(1)

 

目录

一、Linux中线程该如何理解

二、重新定义线程和进程

三、重谈地址空间

四、线程vs进程

五、线程控制

线程创建函数

线程等待函数

线程终止函数

验证线程的tid是什么

创建一批线程

 线程分离

六、互斥锁

锁的原理

锁的应用----封装

七、死锁

八、线程同步


一、Linux中线程该如何理解

1、在Linux中,线程在进程“内部”执行,线程在进程的地址空间内运行(为什么?)

因为任何执行流要执行,都要有资源!(要不然有自己的地址空间,要不然在别人的地址空间里运行)地址空间是进程的资源窗口。所以线程要在进程的地址空间内运行。

2、在Linux中,线程执行粒度要比进程更细?线程执行代码的一部分

cpu只有调度执行流的概念,并不关心执行的是进程还是线程。

二、重新定义线程和进程

什么是线程?线程是操作系统调度的基本单位。

什么是进程?内核观点:进程是承担分配资源的基本实体

如何理解我们以前的进程?操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只有一个执行流!!

复用数据结构和管理算法,struct task_struct ---------Linux没有“真正”(只是没有像Windows一样设置struct tcb)意义上的线程,而是用“进程”(进程的内核数据结构)模拟线程。

cpu只有调度行流的概念  线程 <= CPU执行流 <= 进程    在Linux中的执行流,被叫做执轻量级进程

三、重谈地址空间

对多线程的资源如何进行分配???

cpu内部的cr3寄存器保存页表的地址   物理内存被分为4kb大小的块 

虚拟地址是如何转化为物理地址的??以32位虚拟地址为例   32 = 10 + 10 +12

虚拟地址的前10位数,直接转成10进制数,表示1级页表的下标。 1级页表中存放的是2级页表的地址。 再用10位数转化为10进制找2级页表下标 。  二级页表存放的页框的起始地址。   12位在页框中搜索时某个物理地址的偏移量   2^12 = 4kb 页框的大小就是4kb  一个页表最大是 4kb * 1024 = 1MB ( 二级页表在大部分情况下是不全的)

线程目前分配资源,本质就是分配地址空间范围

四、线程vs进程

线程比进程要更加轻量化(为什么?)

a、创建和释放更加轻量化----线程创建只需要创建task_struct(生死问题)

b、切换更加轻量化----执行的代码变少了、线程切换的时候页表、地址空间不需要切换  切换的时候只是局部切换(只需要几个寄存器的上下文切换) 切换的效率就会更高(运行问题)

cpu中有一个硬件cache 缓存热数据(经常被使用的数据) 线程进行切换的时候这里面的数据大部分是不需要被切换的 因为几个线程使用的地址空间的差别不大  即cache数据不需要由冷变热 不需要重新缓存   而进程进行切换的时候是将这个上面的所有数据进行切换。  

进程是资源分配的基本单位

线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据:(私有)

线程ID、一组寄存器(线程的上下文  独立的上下文保证线程是被独立的调度的)、栈(线程需要独立的栈结构,保证线程之间执行是不会出现执行流错乱的问题)、errno、信号屏蔽字、调度优先级

多个线程共享以下资源:(共享)
文件描述符表、每种信号的处理方式、当前工作目录、用户id和组id

内核中没有明确的线程的概念。  对线程只有轻量级进程的概念。   不会给我直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用!   用户需要线程接口!pthread线程库---应用层---轻量级进程接口进行封装。为用户提供直接线程的接口。  几乎所有的Linux平台都是默认自带这个库的! Linux中编写多线程代码需要使用第三方pthread库!

五、线程控制

线程创建函数

#include <pthread.h>

int  pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start routine) (void*), void *arg);

pthread_t  *thread : pthread_t 其实是一个整数 是一个输出型参数   创建一个线程要知道这个线程的线程id

const pthread_attr_t *attr: 线程的属性 它的栈大小 设置为nullptr就可以了

void *(*start routine) (void*) : 返回值是void* 参数也是void* 的函数指针

返回值                         参数

这个线程会执行这个函数指针指向的方法

 void不能用来定义变量  void*可以用来定义变量  void*可以用来接收或者返回任意类型,常用的是用来接收或返回指针类型,如果一个接口想做到与类型无关 它的类型就是void

void *arg:输入型参数  创建线程成功, 新线程回调线程函数的时候,需要参数。这个参数就是给线程函数传递的。

创建线程成功0被返回 失败以非0的方式告诉我们为什么出错

可以看出线程id (tid) 并不是LWP

新线程执行自己的代码  当新线程函数结束了 代表新线程也就结束了  主线程会继续向后执行。

一个进程内部会有两个执行流  main函数的后续代码 和 新线程指向的新函数   两个使用的是不同的地址空间注定了两者在代码资源上是分离的。

-aL 查看所有的轻量级进程   PID相同 可以看出是同一个进程  的不同执行流  LWP(light weight process)   是轻量级进程的pid  线程调度的时候看的是LWP

PID 等于 LWP 代表这个线程是主线程

任何一个线程(不论是主线程还是新线程)被杀掉了,默认整个进程都会被杀掉。我们可以理解这个杀线程的指令是给进程发的。

//可以被多个执行流同时执行  show函数被重入了
void show(const string &name)
{cout << name << "say# " << "hello thread" << endl;
}void *threadRoutine(void *args)
{while(true){//cout << "new thread, pid: " << getpid() << endl;show("[new thread]");sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);//主线程不退出while(true){//cout << "main thread, pid: " << getpid() << endl;show("[main thread]");sleep(1);}return 0;
}

int g_val = 100;void *threadRoutine(void *args)
{while(true){printf("new thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);//主线程不退出while(true){printf("main thread pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);sleep(1);g_val++;}return 0;
}

主线程修改,新线程也可以看到全局变量的修改。  已初始化或者未初始化的全局变量在所有的线程中是共享的。 因为它们共享地址空间

线程之间的通信 是很容易的 因为它们天然的看到的是同一份资源  定义全局变量就可以

线程等待函数

#include <pthread.h>

int pthread_join (pthread_t  thread,  void  **retval);

pthread_t  thread:要等待线程的id

void  **retval:设为nullptr

返回值:成功返回0,失败返回错误码

新线程被创建也要被等待,否则就会出现类似于僵尸进程的情况,主线程肯定最后退。防止新线程造成内存泄漏   等待线程获取线程的退出结果

void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while(true){printf("%s,new thread pid: %d, g_val: %d, &g_val: 0x%p\n", name, getpid(), g_val, &g_val);//cout << "new thread, pid: " << getpid() << endl;//show("[new thread]");sleep(1);cnt--;if(cnt == 0) break;}return nullptr; //走到这里,默认线程退出了
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");pthread_join(tid, nullptr);cout << "main thread quit...." << endl;  //main thread 等待的时候, 默认是阻塞等待的!return 0;
}

新线程不退出主线程也不会退出

void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while(true){printf("%s,new thread pid: %d, g_val: %d, &g_val: 0x%p\n", name, getpid(), g_val, &g_val);//cout << "new thread, pid: " << getpid() << endl;//show("[new thread]");sleep(1);cnt--;if(cnt == 0) break;}return (void*)1; //走到这里,默认线程退出了
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");void *retval;pthread_join(tid, &retval);  //main thread等待的时候,默认是阻塞等待的!  
//为什么我们在这里join的时候不考虑异常呢? 做不到! 因为异常的时候主线程就会崩掉cout << "main thread quit...., ret: " << (long long int)retval << endl;  //main thread 等待的时候, 默认是阻塞等待的!return 0;
}

线程终止函数

不能用exit()终止线程  它是用来终止进程的

#include <pthread.h>

void pthread_exit(void *retval);

谁调用就终止谁

void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while(true){printf("%s,new thread pid: %d, g_val: %d, &g_val: 0x%p\n", name, getpid(), g_val, &g_val);//cout << "new thread, pid: " << getpid() << endl;//show("[new thread]");sleep(1);cnt--;if(cnt == 0) break;}pthread_exit((void*)100);//return (void*)1; //走到这里,默认线程退出了
}

 线程取消

int main()
{//PTHREAD_CANCELD;pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");sleep(1);//只是为了保证新线程已经启动pthread_cancel(tid);//线程取消void *retval;pthread_join(tid, &retval);  //main thread等待的时候,默认是阻塞等待的!  为什么我们在这里join的时候不考虑异常呢? 做不到! 因为异常的时候主线程就会崩掉cout << "main thread quit...., ret: " << (long long int)retval << endl;  //main thread 等待的时候, 默认是阻塞等待的!return 0;
}

线程是被取消的,它的返回值就是-1

线程的参数和返回值

线程函数的参数不仅仅可以传整数或字符串还可以传对象!

class Request
{public:Request(int start, int end, const string &threadname):start_(start), end_(end), threadname_(threadname){}public:int start_;int end_;string threadname_;
};class Response
{public:Response(int result, int exitcode):result_(result),exitcode_(exitcode){}public:int result_; //计算结果int exitcode_; //计算结果是否可靠
};void *sumCount(void* args)
{Request *rq = static_cast<Request*>(args); //对args的类型进行强转Response *rsp = new Response(0,0);  //构建一个Response对象for(int i = rq->start_; i < rq->end_; i++){rsp->result_ += i;}delete rq; //申请的堆空间释放掉return rsp;
}
int main()
{pthread_t tid;Request *rq = new Request(1, 100, "thread 1");    //1到100的求和 ,线程的名字设置为线程1  //构造一个request对象pthread_create(&tid, nullptr, sumCount, rq);  //传入rq就是将一个请求传给线程  不仅仅可以传整数字符串 还可以传类的对象void *ret;pthread_join(tid, &ret); //拿到该线程执行的结果  即堆空间的地址Response *rsp = static_cast<Response*>(ret);cout << "rsp->result: " << rsp->result_ << ", exitcode: " << rsp->exitcode_ << endl;delete rsp; //打印完释放掉堆空间return 0;}

上面的代码,在主线程中new一个对象传给新线程,在新线程上new一个对象传给主线程。说明堆上的数据在线程间是共享的。

c++11里面的多线程其实是封装的原生线程库

验证线程的tid是什么

#include <pthread.h>
pthread_t pthread_self(void);  //获取调用线程的id

std::string toHex(pthread_t tid)
{char hex[64]; //定义一个缓冲区snprintf(hex,sizeof(hex), "%p", tid);return hex;
}

使用snprintf进行格式化输出。tid的十六进制表示保存在hex数组中。

snprintf是将格式化的数据输出到指定数组中。

hex:是输出目标数组,即存储格式化之后的值

sizeof(hex):目标数组的大小,防止溢出缓冲区,保证最多填充64个字节

%p:格式化字符串,将给定的指针按照十六进制的形式输出

这个函数将 pthread_t 类型的线程 ID 转换为其十六进制字符串表示。它通过 snprintf 格式化线程 ID 并返回一个包含该十六进制表示的 std::string 对象。

clone接口的两个重要参数 函数指针(回调函数)、自定义栈 因为参数太多,我们不直接用这个接口,它被线程库封装。我们用的是 pthread_create、 pthread_join......这样的接口。

每一个线程都要提供线程的执行方法,我们会在线程库内部开辟空间,把对应的方法交给回调函数,把栈交给自定义的栈。也就是说线程的概念是库给我们维护的。线程库要维护线程概念,不用维护线程的执行流。   所以当执行多线程代码的时候库要不要加载到内存里??库要加载到内存,通过页表给我们映射到共享区。  线程库中要维护多个线程属性集合,线程库要管理这些线程。先组织,再描述。

存的是地址、在用户空间、是虚拟地址可以直接访问。要获得一个线程的属性直接拿它的tid,把地址传进去就可以直接找到属性。tid本质上就是pthread库在地址空间某个地区的地址  tid线程控制块在库当中的地址

每个线程在运行的时候都要有独立的栈结构,每一个线程都会有自己的调用链。这个栈结构会保存每个执行流在执行时的临时变量   主线程直接用地址空间中提供的栈结构即可。  剩下的轻量级进程,首先在库里面为新线程创建一个方块,(描述这个线程的线程控制块)起始地址作为自己线程的id,这个块里包含一个默认大小的空间,叫作线程栈。  在内核中创建执行流,   把这个线程栈传给clone,所以clone在执行时形成的临时数据都会被压入到这个线程库在应用层的栈结构中

除了主线程,所有其他线程的独立栈,都在共享区,具体来说是在pthread库中,tid指向的用户tcb中    所有的非主线程它的栈都在库当中进行维护,即共享区中维护。

用户级线程+内核的LWP ---->Linux线程

栈 :  每一个执行流的本质都是一条调用链。  支持在应用层完成整个调用链所对应的临时变量的空间的开辟和释放。新创建的线程和主线程在执行流上是独立的。因此,每一个都要有一个属于自己独立的栈结构,让自己的调用链不受别人的干扰。

创建一批线程

#define NUM 10struct threadData
{string tid;string threadname;
};void *InitThreadData(threadData *td,int number, pthread_t tid)
{td->threadname = "thread-" + to_string(number);char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", tid); //将tid转化为十六进制td->tid = buffer;
}void *threadRoutine(void *args)
{threadData *td = static_cast<threadData *>(args);//要用传进来的参数就要首先对他进行强转int i = 0;while(i < 10){//输出线程的id和线程的名字cout << "pid: " << getpid() << ",tid : " << td->tid << ", threadname: " << td->threadname << endl;sleep(1);i++;}delete td;return nullptr;
}int main()
{vector<pthread_t> tids;//保存所有的线程id//批量创建线程for(int i = 0; i < NUM; i++){pthread_t tid;// threadData td; //这样写是不对的  因为这个是在主线程的栈里面  这是一个临时变量 for循环执行完之后就会被释放// td.threadname = // td.tid =threadData *td = new threadData; //td变量指向的内容是堆空间  每一次for循环都会重新创建一次堆空间//到时候每个线程都可以访问堆空间 但是它们访问的是堆空间中不同的区域//所有的线程是共享堆空间的InitThreadData(td, i, tid); //每一个创建一个线程,都创建一个线程数据都初始化一下,然后传递给这个线程,所以线程一进到线程函数就拿到了自己的数据pthread_create(&tid, nullptr, threadRoutine, td);tids.push_back(tid); //线程创建好之后把新的线程id给保存到tids中 }//等待每一个线程for(int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr);}return 0;
}

每个线程都会有自己独立的栈结构! 其实线程和线程之间,几乎没有秘密,线程的栈上的数据,也是可以被其他线程看到并访问的。 全局变量是可以被所有线程同时看到并访问的。

__thread int g_val = 100;  线程局部存储!  只能定义内置类型,不能用来修饰自定义类型。

 线程分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

int  pthread_detach(pthread_t  thread)    传入要分离线程的id

pthread_detach(pthread_self());

六、互斥锁

寄存器不等于寄存器的内容  寄存器只有一套,但是每个线程都要有寄存器对应的数据

线程在执行的时候,将共享数据加载到CPU寄存器的本质:把数据的内容,变成了自己的上下文----以拷贝的方式,给自己单独拿了一份。

#define NUM 4class threadData
{public:threadData(int number){threadname = "thread-" + to_string(number);}public:string threadname;
};int tickets = 1000;void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while(true){if(tickets > 0) //四个线程并发的抢票  同一个全局变量tickets{usleep(1000);printf("who=%s,get a ticket: %d\n", name, tickets); //数据被多线程并发访问造成了数据不一致问题tickets--; //对一个全局变量进行多线程并发--/++ 操作是不安全的。}elsebreak;}printf("%s ... quit\n",name);return nullptr;
}int main()
{vector<pthread_t> tids; //存放创建线程的tidvector<threadData*> thread_datas; //存放线程函数的参数for(int i = 1; i <= NUM; i++){pthread_t tid;threadData* td = new threadData(i); //线程参数 是一个自定义类型  线程名字 这样传递可以对参数进行扩展thread_datas.push_back(td);pthread_create(&tid,nullptr,getTicket,thread_datas[i-1]);//创建线程tids.push_back(tid);}for(auto thread : tids){pthread_join(thread, nullptr);}for(auto td : thread_datas){delete td;}return 0;
}

由结果可以看出,1000张票,最后卖到了-2张。  这就是数据被多线程并发访问造成的。

解决办法:对共享数据的任何访问,保证任何时候只有一个执行流访问!!----- 互斥!!! 即使用锁。

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex *mutex);------对一个锁进行释放

pthread_mutex:只是库给我们提供的数据类型

定义初始化一把常见的锁: int pthread_mutex_init (pthread_mutex_t *restrict mutex,  const pthread_mutexattr_t  *restrict  sttr);

pthread_mutex_t *restrict mutex:用函数对锁进行初始化

const pthread_mutexattr_t  *restrict  sttr:锁的属性,默认设置为null

#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;   如果将锁定义为全局的我们可以用宏来进行初始化。  这样是不需要destroy(释放)的

加锁和解锁之间我们就可以保证是安全访问的
int  pthread_mutex_lock(pthread_mutex_t  *mutex); ----  加锁

pthread_mutex_t  *mutex: 参数是锁的地址

申请锁失败(发现锁被别人拿走了 不返回进行阻塞自己)   

int  pthread_mutex_trylock(pthread_mutex_t  *mutex);  

申请锁失败,立即返回   返回值代表它申请成功还是失败

int  pthread_mutex_unlock(pthread_mutex_t  *mutex) ---- 解锁

class threadData  //这是要给线程函数传递的参数
{public:threadData(int number, pthread_mutex_t *mutex){threadname = "thread-" + to_string(number);lock = mutex;}public:string threadname; //线程名字pthread_mutex_t *lock; //锁
};int tickets = 1000;void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();//线程代码只有一部分是访问临界资源(tickets)的(访问临界资源的的代码叫作临界区)//加锁是线程对临界区代码串行执行//加锁的本质:用时间换安全//加锁原则:尽量保证临界区的代码,越少越好while(true){//线程对于锁的竞争能力可能不同//每个线程在抢票之前都要申请锁  那么锁本身就是共享资源  但是我们在申请锁的时候不用担心多线程并发问题  因为申请锁和释放本身设计就是原子性操作。pthread_mutex_lock(td->lock); //加锁  申请锁成功才能继续往后执行,不成功,阻塞等待if(tickets > 0) //四个线程并发的抢票  同一个全局变量tickets{usleep(1000);printf("who=%s,get a ticket: %d\n", name, tickets); //数据被多线程并发访问造成了数据不一致问题tickets--; //对一个全局变量进行多线程并发--/++ 操作是不安全的。pthread_mutex_unlock(td->lock); //解锁}else{pthread_mutex_unlock(td->lock); //解锁   先解锁再break 因为如果先break就会有锁资源没有释放 造成别的执行流申请锁资源失败break;}usleep(13);  //加sleep之后 就是在这个线程释放完锁之后  先sleep一下  不去申请锁 让别的线程去申请//纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!但是不是只要有互斥,必有饥饿。//pthread_mutex_unlock(td->lock); //解锁}printf("%s ... quit\n",name);return nullptr;
}
int main()
{pthread_mutex_t lock;  //申请一个锁  在main函数的栈上开辟空间pthread_mutex_init(&lock, nullptr); //初始化vector<pthread_t> tids; //存放创建线程的tidvector<threadData*> thread_datas; //存放线程函数的参数//让所有的线程看到的是同一把锁for(int i = 1; i <= NUM; i++){pthread_t tid;threadData* td = new threadData(i, &lock); //线程参数 是一个自定义类型  线程名字 再给每一个创建的线程一把锁 这样传递可以对参数进行扩展//如果定义一把全局的锁  就不用给每一个线程都给一把锁了thread_datas.push_back(td);pthread_create(&tid,nullptr,getTicket,thread_datas[i-1]);//创建线程tids.push_back(tid);}for(auto thread : tids){pthread_join(thread, nullptr);}for(auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock); //释放锁return 0;
}

1、加锁是线程对临界区代码串行执行

2、加锁的本质:用时间换安全

3、加锁原则:尽量保证临界区的代码,越少越好

因为:串行执行的比例就会降低,因为在临界区线程也会被调度,临界区的代码越短,线程被调度的概率就越小,那么让其他线程等锁的时间就会减少。

4、纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!但是不是只要有互斥,必有饥饿。  适合纯互斥的场景就用互斥。观察员制定规则:1、外面来的人必须排队                                                  2、出来的人,不能立马重新申请锁,必须排到队列的尾部 

让所有的线程获取锁,要按照一定的顺序。(避免当一个人出来之后将外面的所有人都唤醒,但是只有一把钥匙,这样会造成无效唤醒)

按照一定的顺序性获取资源-----叫作同步!!

5、锁本身就是共享资源! 所以,申请锁和释放锁本身就被设计成为了原子性操作。

6、在临界区里面线程是可以被切换的。但是并不害怕被切换,因为申请锁了之后并没有释放锁,在被切出去时候,是持有锁被切走的。  在我不在的时候照样没有人进入临界区访问临界资源。  对其他线程来讲,关注的是一个线程没有锁或者一个线程释放锁,他并不关心他持有锁的过程。也就是说,当前线程访问临界区的过程,对于其他线程是原子的!

锁的原理

tickets---不是原子的。因为它会变成3条汇编语句。  原子的:我们可以理解为一条汇编语句就是原子的    cpu中的数据和内存中的数据进行切换    把一个共享的变量(公共的锁)交换到一个线程自己的上下文

锁的应用----封装

 LockGuard.hpp

#pragma once#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *lock):lock_(lock) //用外面传入的锁初始化类内的锁{}void Lock(){pthread_mutex_lock(lock_);//对类内的锁进行加锁}void Unlock(){pthread_mutex_unlock(lock_);//对类内的锁进行解锁}~Mutex(){}
private:pthread_mutex_t *lock_;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock):mutex_(lock){mutex_.Lock(); //构造这个对象的时候进行加锁}~LockGuard(){mutex_.Unlock(); //在析构这个对象的时候进行解锁}
private:Mutex mutex_;
};

 mythread.cc

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include "LockGuard.hpp"using namespace std;#define NUM 4pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;  //定义一个全局的锁class threadData  //这是要给线程函数传递的值  可以增加
{public:threadData(int number){threadname = "thread-" + to_string(number);}public:string threadname; //线程名字};int tickets = 1000;void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();//线程代码只有一部分是访问临界资源(tickets)的(访问临界资源的的代码叫作临界区)//加锁是线程对临界区代码串行执行//加锁的本质:用时间换安全//加锁原则:尽量保证临界区的代码,越少越好while(true){{  //明确临界区 完成加锁解锁LockGuard lockguard(&lock); //定义临时的lockguard对象   定义对象的时候就调用封装的加锁函数  当while循环结束的时候这个临时对象会被自动释放,就会自动调用析构进行自动解锁//用对象的生命周期来管理加锁和解锁  RAII风格的锁if(tickets > 0) //四个线程并发的抢票  同一个全局变量tickets{usleep(1000);printf("who=%s,get a ticket: %d\n", name, tickets); //数据被多线程并发访问造成了数据不一致问题tickets--; //对一个全局变量进行多线程并发--/++ 操作是不安全的。}elsebreak;} //usleep(13);  //加sleep之后 就是在这个线程释放完锁之后  先sleep一下  不去申请锁 让别的线程去申请//纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题!但是不是只要有互斥,必有饥饿。}printf("%s ... quit\n",name);return nullptr;
}
int main()
{pthread_mutex_t lock;  //申请一个锁  在main函数的栈上开辟空间vector<pthread_t> tids; //存放创建线程的tidvector<threadData*> thread_datas; //存放线程函数的参数//让所有的线程看到的是同一把锁for(int i = 1; i <= NUM; i++){pthread_t tid;threadData* td = new threadData(i); //线程参数 是一个自定义类型  线程名字 再给每一个创建的线程一把锁 这样传递可以对参数进行扩展thread_datas.push_back(td);pthread_create(&tid,nullptr,getTicket,thread_datas[i-1]);//创建线程tids.push_back(tid);}for(auto thread : tids){pthread_join(thread, nullptr);}for(auto td : thread_datas){delete td;}return 0;
}

//定义临时的lockguard对象   定义对象的时候就调用封装的加锁函数  当结束的时候这个临时对象会被自动释放,就会自动调用析构进行自动解锁
//用对象的生命周期来管理加锁和解锁  RAII风格的锁

七、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
单执行流,只有一把锁,也可能出现死锁的情况。 eg:申请两次,但没有释放
死锁的四个必要条件(要产生死锁四个条件都必须同时满足)
1、互斥条件:一个资源每次只能被一个执行流使用  (可以理解为产生死锁就是因为有锁,有锁就是为了满足互斥)
2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(拿着我的锁,申请对方的锁)
3、不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺(不能强对方的锁)
4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
解决死锁问题:
请求与不保持:用trylock申请锁  申请第二把锁失败就立马返回  释放第一把 ---通过接口
剥夺:释放对方的锁 ---- 通过接口来完成
破坏环路问题:申请锁的时候按照顺序进行申请   ---->  加锁顺序一致、
避免锁未释放场景、 资源一次性分配
避免死锁算法:银行家算法、死锁检测算法

八、线程同步

同步问题是保证数据安全的情况下,让我们的线程访问资源具有一定的顺序性。

那么已经有顺序,在排队了,为什么还要加锁???  因为排队是结果,加锁是为了防止有突然来的人,他对申请了一下,发现有人才回去排队(排队是被迫的)

互斥本身可以解决一类问题,只不过它有局限性,比如调度不均衡、竞争不均衡而导致的饥饿问题。我们是用同步来解决互斥存在的问题的。    我们不要理解为互斥本身是一种问题,它其实是一种解决方案。

条件变量:是一个结构体  通知机制+队列     条件变量必须依赖于锁的使用

条件变量函数   

初始化:int  pthread_cond_init(pthread_cond_t  *restrict  cond,    const  pthread_condattr_t  *restrict attr)

cond:要初始化的条件变量

attr:NULL

销毁:

int  pthread_cond_destroy(pthread_cond_t   *cond)

定义全局的条件变量

pthread_cond_t  cond  =  PTHREAD_COND_INITIALIZER

当申请锁失败的时候将自己投入到阻塞队列中

int  pthread_cond_wait(pthread_cond_t  *restrict cond,  pthread_mutex_t  *restrict  mutex);

cond:要在这个条件变量上等待

mutex:互斥量

唤醒等待:

int  pthread_cond_broadcast(pthread_cond_t  *cond);

唤醒所有在这个条件变量的线程

int  pthread_cond_signal(pthread_cond_t  *cond);

唤醒一个线程

#include<iostream>
#include<pthread.h>
#include<unistd.h>int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义一把锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;   //定义一个条件变量void *Count(void *args)
{pthread_detach(pthread_self()); //线程对自己进行分离uint64_t number = (uint64_t)args;std::cout << "pthread: " << number << " create success" << std::endl;while(true){pthread_mutex_lock(&mutex);//我们怎么知道要让一个线程去休眠??一定是临界资源不就绪,临界资源也是有状态的!//我们怎么知道临界资源是就绪还是不就绪的?? 判断出来的。判断也是访问临界资源。也就是判断必须在加锁之后。pthread_cond_wait(&cond, &mutex);  //1、 pthread_cond_wait让线程等待的时候,会自动释放锁std::cout << "pthread: " << number << ", cnt: " <<cnt++ << std::endl;  //多个线程向显示器进行打印 显示器也属于共享资源  打印错乱很正常pthread_mutex_unlock(&mutex);//sleep(3);  //加sleep可以让线程执行完之后等待,让其他线程去执行}
}int main()
{for(uint64_t i = 0; i < 5; i++)  //无符号长整数{pthread_t tid;pthread_create(&tid, nullptr, Count, (void*)i);  //i没有传地址  只是进行了拷贝  如果传了地址新线程和主线程用的是同一个i,无法保证是线程函数先用i还是i先++}sleep(3);std::cout << "main thread ctrl begin: " << std::endl;while(true)  //确保主线程最后退出{sleep(1);pthread_cond_signal(&cond); //唤醒在cond的等待队列中等待的一个线程,默认都是第一个std::cout << "signal one thread..." << std::endl;}return 0;
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词