目录
[2.1 条件变量](#2.1 条件变量)
[2.1.4.1 BlockingQueue](#2.1.4.1 BlockingQueue)
2.1.5为什么pthread_cond_wait需要互斥量?
1.线程互斥
1.1进程线程间的互斥相关概念
共享资源
临界资源 :多线程执行流被保护的且需要互斥的共享的资源就叫做临界资源
临界区 :每个线程内部,访问临界资源的代码就叫做临界区
互斥 :任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1.2互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
我们先看一个例子(例子里会用到我上一篇文章线程封装的v2版本,但不需要理解,可以理解为就是pthread库的使用,稍微有变化,那就是把Thread类里的_data成员也变成引用类型了,构造函数里data的类型不是const T,是单纯的T。)
Thread.hpp
cpp#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<functional> namespace ThreadModule{ template<class T> using func_t =std::function<void(T)>; using std::cout; using std::cin; using std::endl; using std::string; template<class T> class Thread{ public: Thread(func_t<T> func,T data,const string&name="default") :_func(func),_data(data),_threadname(name),_stop(true) {} void Excute(){ _func(_data); } static void *threadrun(void*arg)//注意,类成员函数有this指针,跟线程库的要求冲突了 //这里采取静态函数的方式,这样就没有this指针了,另外也可以放在类外面 { //注意,因为_func和_data都是成员函数,访问需要this指针 //但静态函数又没有this指针,那么就只能把this指针当参数传进threadrun函数。 //然后将方法的调用封装在Excute这个成员函数里 //这里就只需要用this指针直接调用Excute函数即可。 Thread<T> *self=static_cast<Thread*>(arg); self->Excute(); return nullptr; } bool Start(){ int n=pthread_create(&_tid,nullptr,threadrun,this); if(!n){ _stop=false; return true; } else{ return false; } } void Detach(){ if(!_stop){ pthread_detach(_tid); } } void Join(){ if(!_stop){ pthread_join(_tid,nullptr); } } void Stop(){_stop=true;} string name(){return _threadname;} ~Thread(){} private: pthread_t _tid; string _threadname; T _data; func_t<T> _func; bool _stop; }; } #endif
cpp#include"Thread.hpp" #include<vector> using namespace ThreadModule; int g_tickets=10000;//共享资源,没有保护 const int num=4; class ThreadData{ public: ThreadData(int &tickets,string name) :_tickets(tickets),_name(name),total(0) {} public: int &_tickets;//用的是同一个全局变量g_tickets string _name; int total; }; void route(ThreadData*td){ while(true){ if(td->_tickets>0){ usleep(1000); cout<<td->_name<<" running, "; cout<<"get ticket: "<<td->_tickets--<<endl; td->total++; } else{ break; } } } int main(){ std::vector<Thread<ThreadData*>>threads; std::vector<ThreadData*>tds; //创建一批线程 for(int i=0;i<num;i++){ string name="thread-"+std::to_string(i+1); ThreadData *td=new ThreadData(g_tickets,name);//保证对象不会因为循坏而销毁 threads.emplace_back(route,td,name); tds.emplace_back(td); } //启动一批线程 for(auto &thread:threads){ thread.Start(); } //等待一批线程 for(auto &thread:threads){ thread.Join(); } //输出统计信息 for(auto td:tds){ cout<<td->_name<<" : "<<td->total<<endl; delete td; } return 0; }因为没有保护,当票数接近0的时候,正好有几个线程都已经经过了if判断(也就是进入了临界区),当这些线程都完成--操作,就导致票数低过了0,因为这些进入了临界区的线程数量超过了共享资源票数。
为什么可能无法获得正确结果(这里就是数据不一致)?
if语句判断条件为真以后,代码可以并发的切换到其他线程
usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
tickets--操作本身就不是一个原子操作tickets--操作并不是原子操作,而是对应三条汇编指令 :
load: 将共享变量ticket从内存加载到寄存器中
update: 更新寄存器里面的值,执行-1操作
**store:**将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点 :
代码必须要有互斥行为: 当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区 。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区 。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。另外原子操作,从编码角度,就是一条高级语言语句对应一条汇编语句,比如a=10,变成汇编就是一个mov语句。
互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
方法1,静态or全局分配:
cpppthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER方法2,动态(局部变量)分配:
cppint pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); 参数: mutex:要初始化的互斥量 attr: NULL
销毁互斥量
销毁互斥量需要注意:
使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
cppint pthread_mutex_destroy(pthread_mutex_t *mutex); 和前面的创建一样,成功返回0,错误,就设置错误码
互斥量加锁和解锁
cppint pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex) ; 返回值:成功返回0,失败返回错误号调用 pthread_mutex_lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
另外,还有个pthread_mutex_trylock,他唯一的区别就是申请锁失败(非函数失败)不会进入阻塞,而是马上返回错误号,然后继续执行执行流。
注意,锁是一个规则,规则就是要所有线程都遵守才有意义的,否则就起不到保护的作用了。
改进前面的例子
改进方法就是给if判断的代码套个锁。加锁的本质是把并行执行变串行执行,加锁的粒度要越细越好(不要把非临界资源给套进去了)
cpppthread_mutex_t mx=PTHREAD_MUTEX_INITIALIZER; void route(ThreadData*td){ while(true){ pthread_mutex_lock(&mx); if(td->_tickets>0){ usleep(1000); cout<<td->_name<<" running, "; cout<<"get ticket: "<<td->_tickets--<<endl; pthread_mutex_unlock(&mx); td->total++; } else{ pthread_mutex_unlock(&mx); break; } } }可以发现,加了锁之后,抢票的总额是固定的10000,不会超出。
但上面的环境是ubuntu20.xx,但我们如果在centos7.6下运行,很可能出现一个线程把所有票都抢了,不会如上面这么平均,说明不同的系统,越新的系统调度算法越好,这样才能让每个线程都公平的运行。
也就是说如果一个线程的竞争锁的能力太强的话,会让其他线程都抢不到锁,造成其他线程的饥饿问题(进程/线程饥饿问题)。
另外也不一定要全局设个锁,我们局部锁,然后传参引用,具体看下面
cpp#include"Thread.hpp" #include<vector> using namespace ThreadModule; int g_tickets=10000;//共享资源,没有保护 const int num=4; //pthread_mutex_t mx=PTHREAD_MUTEX_INITIALIZER; class ThreadData{ public: ThreadData(int &tickets,string name,pthread_mutex_t &mutex) :_tickets(tickets),_name(name),total(0),_mutex(mutex) {} public: int &_tickets;//用的是同一个全局变量g_tickets string _name; int total; pthread_mutex_t &_mutex; }; void route(ThreadData*td){ while(true){ //pthread_mutex_lock(&mx); pthread_mutex_lock(&td->_mutex); if(td->_tickets>0){ usleep(1000); cout<<td->_name<<" running, "; cout<<"get ticket: "<<td->_tickets--<<endl; //pthread_mutex_unlock(&mx); pthread_mutex_unlock(&td->_mutex); td->total++; } else{ pthread_mutex_unlock(&td->_mutex); //pthread_mutex_unlock(&mx); break; } } } int main(){ pthread_mutex_t mutex; pthread_mutex_init(&mutex,nullptr); std::vector<Thread<ThreadData*>>threads; std::vector<ThreadData*>tds; //创建一批线程 for(int i=0;i<num;i++){ string name="thread-"+std::to_string(i+1); ThreadData *td=new ThreadData(g_tickets,name,mutex);//保证对象不会因为循坏而销毁 threads.emplace_back(route,td,name); tds.emplace_back(td); } //启动一批线程 for(auto &thread:threads){ thread.Start(); } //等待一批线程 for(auto &thread:threads){ thread.Join(); } //输出统计信息 for(auto td:tds){ cout<<td->_name<<" : "<<td->total<<endl; delete td; } pthread_mutex_destroy(&mutex); return 0; }
1.3互斥量实现原理
经过上面的例子,大家已经意识到单纯的i++或者++i都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
cpplock: movb $0, %al xchgb %al, mutex if(al寄存器的内容 > 0){return 0;} else 挂起等待; goto lock; unlock: movb $1, mutex 唤醒等待Mutex的线程; return 0;lock 操作:
mutex默认置1,把al寄存器的内容置为0
先用 xchgb原子交换 mutex 和 al 的值
如果交换后 al > 0,表示之前 mutex=1(未锁定),现在获取成功
如果 al ≤ 0,表示之前 mutex=0(已锁定),需要等待
unlock 操作:
直接将 mutex 设为 1
唤醒等待线程
注意,寄存器的内容是独属于线程的,而内存中的变量是所有线程共享的。那么只要把mutex这个互斥量(存在内存中)的内容跟某个寄存器(默认0)里的进行交换,比如线程1交换了,且因为交换只有一条汇编语句,保证不会被其他线程打断,进程1的上下文数据中的al寄存器里存的就是mutex互斥量1,此时交换完成了,然后又有个线程2来加锁,线程1切换(顺便把上下文数据都带走了),这时候,因为内存中的mutex互斥量的值是0,所以线程2挂起等待。直到线程1解锁,然后把内存中的mutex互斥量置为1,并唤醒等待互斥量的线程,让他们继续争锁。
最后,锁其实还有很多实现细节,比如周期什么的,这里不多探究了
1.4互斥相关的封装
我们这里为了简单点,就不对互斥量进行封装了,对加锁解锁的过程进行封装。
LockGuard.hpp
cpp#ifndef __LOCK_GUARD_HPP__ #define __LOCK_GUARD_HPP__ #include<iostream> #include<pthread.h> class LockGuard{ public: LockGuard(pthread_mutex_t *mutex):_mutex(mutex){ pthread_mutex_lock(_mutex); } ~LockGuard(){ pthread_mutex_unlock(_mutex); } private: pthread_mutex_t *_mutex; }; #endif
cpp#include"Thread.hpp" #include<vector> #include"LockGuard.hpp" using namespace ThreadModule; int g_tickets=10000;//共享资源,没有保护 const int num=4; class ThreadData{ public: ThreadData(int &tickets,string name,pthread_mutex_t &mutex) :_tickets(tickets),_name(name),total(0),_mutex(mutex) {} public: int &_tickets;//用的是同一个全局变量g_tickets string _name; int total; pthread_mutex_t &_mutex; }; void route(ThreadData*td){ while(true){ LockGuard guard(&td->_mutex);//RAII风格的加锁和解锁 if(td->_tickets>0){ usleep(1000); cout<<td->_name<<" running, "; cout<<"get ticket: "<<td->_tickets--<<endl; td->total++; } else{ break; } } } int main(){ pthread_mutex_t mutex; pthread_mutex_init(&mutex,nullptr); std::vector<Thread<ThreadData*>>threads; std::vector<ThreadData*>tds; //创建一批线程 for(int i=0;i<num;i++){ string name="thread-"+std::to_string(i+1); ThreadData *td=new ThreadData(g_tickets,name,mutex);//保证对象不会因为循坏而销毁 threads.emplace_back(route,td,name); tds.emplace_back(td); } //启动一批线程 for(auto &thread:threads){ thread.Start(); } //等待一批线程 for(auto &thread:threads){ thread.Join(); } //输出统计信息 for(auto td:tds){ cout<<td->_name<<" : "<<td->total<<endl; delete td; } pthread_mutex_destroy(&mutex); return 0; }因为guard是个局部对象,生命周期就是一次循坏,循坏结束自动调用析构函数。
所以我们可以在创建的时候加锁,析构的时候解锁。
这种风格称为RAII。我C++的文章里有提及,另外c++是有现成的,比如mutex就是对pthread_mutex_t的封装,lock_guard就是我这里的LockGuard
1.5补充
注意,在临界区内部的线程,也可以被进程切换切走(其他线程依旧阻塞或压根就没申请锁)。
注意,因为临界区被锁保护了起来,所以临界区的代码对于没申请到锁的线程来说也是原子的,只有完成与没完成2种状态,再加上访问临界资源的操作是原子的,结合起来,整个程序就是线程安全的!
2.线程同步
2.1 条件变量
用来实现线程同步的
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
注意,如果没有条件变量,那么线程只能不停地申请锁来进行检测队列里是否有节点,但这样的话,很有可能造成其他线程出现饥饿问题,因为这个线程一直在申请锁,导致其他线程申请不到锁。比如有一个线程打算申请锁放入节点,但是因为另一个线程为了检测队列里有没有节点一直申请锁,导致这个线程申请不到锁,导致队列里一直没有节点,这个疯狂申请锁的线程也就一直在做无用功(效率低下)。
这时候条件变量就好比铃铛+一个附带的等待队列。
当好几个线程访问队列(临界资源),如果没有需要的节点就跑去这个等待队列里,直到有个线程访问队列不是为了访问节点而是为了增加节点,当增加完节点,就会敲一下铃铛,顺便解锁,这时候等待队列的头节点线程就会离开等待队列,申请锁,如果申请到了就访问队列。
这样这些线程对临界资源的访问,就不再是野蛮的争抢,而是有一定顺序性的争抢。
2.1.1同步概念与竞态条件
同步:在保证数据安全(互斥是一种)的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解(像抢票)
2.1.2条件变量函数
pthread_cond_t就是条件变量
初始化
cppint pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数: cond:要初始化的条件变量 attr: NULL pthread_cond_t cond=PTHREAD_COND_INITIALIZER; 局部or全局销毁
cppint pthread_cond_destroy(pthread_cond_t *cond)等待条件满足
cppint pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数: cond:要在这个条件变量上等待 mutex:互斥量 这个接口第一个作用就是让调用该接口的线在cond条件变量上等待,等待的同时解锁(mutex) 当被唤醒之后,竞争锁(mutex) 还有个timedwait是设置了超时时间,超过一定时间自己就醒了。 而wait是只要不叫,就不会醒来唤醒等待
cppint pthread_cond_broadcast(pthread_cond_t *cond) ; int pthread_cond_signal(pthread_cond_t *cond);第一个是全唤醒,第二个是唤醒一个线程。(注意,并不完全唤醒,这些线程还需要去竞争锁)
上面的接口,全是成功返回0,错误返回错误码。
下面是例子,一次唤醒一个线程
cpp#include <iostream> #include <string> #include <pthread.h> #include <vector> #include <unistd.h> using namespace std; pthread_cond_t gcond=PTHREAD_COND_INITIALIZER; pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER; void *SlaverCore(void *args){ string name = static_cast<const char *>(args); while (true) { pthread_mutex_lock(&gmutex);//不管是做什么,访问临界资源前都要加锁 pthread_cond_wait(&gcond,&gmutex);//因为阻塞等待的话,需要让别的线程也能进来,必须先把gmutex释放掉 cout<<"当前被叫醒的线程:"<<name<<endl; pthread_mutex_unlock(&gmutex); } } void *MasterCore(void *args) { sleep(5); cout<<"master 开始工作..."<<endl; string name = static_cast<const char *>(args); while (true) { pthread_cond_signal(&gcond);//唤醒一个阻塞等待队列首部的线程 cout<<"master 唤醒一个线程..."<<endl; sleep(1); } } void StartMaster(vector<pthread_t> *ptids) { pthread_t tid; int n = pthread_create(&tid, nullptr, MasterCore, (void *)"Master Thread"); if (n == 0) { cout << "create Master Thread success" << endl; ptids->emplace_back(tid); } } void StartSlaver(vector<pthread_t> *ptids, int threadnum = 3) { for (int i = 0; i < threadnum; i++) { char *name = new char[64]; snprintf(name, 64, "slaver-%d", i + 1); pthread_t tid; int n = pthread_create(&tid, nullptr, SlaverCore, (void *)name); if (n == 0) { cout << "create "<<name<<" Thread success" << endl; ptids->emplace_back(tid); } } } void WaitThread(vector<pthread_t> *ptids){ for(auto &tid:*ptids){ pthread_join(tid,nullptr); } } int main() { vector<pthread_t> tids; StartMaster(&tids); StartSlaver(&tids, 5); WaitThread(&tids); return 0; }注意,当调度的时候,第一轮是从12345入等待队列的话,那么后面的顺序就固定在了1234512345.....
下面是一次唤醒所有等待的进程
cpp#include <iostream> #include <string> #include <pthread.h> #include <vector> #include <unistd.h> using namespace std; pthread_cond_t gcond=PTHREAD_COND_INITIALIZER; pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER; void *SlaverCore(void *args){ string name = static_cast<const char *>(args); while (true) { pthread_mutex_lock(&gmutex);//不管是做什么,访问临界资源前都要加锁 pthread_cond_wait(&gcond,&gmutex);//因为阻塞等待的话,需要让别的线程也能进来,必须先把gmutex释放掉 cout<<"当前被叫醒的线程:"<<name<<endl; pthread_mutex_unlock(&gmutex); } } void *MasterCore(void *args) { sleep(5); cout<<"master 开始工作..."<<endl; string name = static_cast<const char *>(args); while (true) { //pthread_cond_signal(&gcond);//唤醒一个阻塞等待队列首部的线程 pthread_cond_broadcast(&gcond); cout<<"master 唤醒一个线程..."<<endl; sleep(1); } } void StartMaster(vector<pthread_t> *ptids) { pthread_t tid; int n = pthread_create(&tid, nullptr, MasterCore, (void *)"Master Thread"); if (n == 0) { cout << "create Master Thread success" << endl; ptids->emplace_back(tid); } } void StartSlaver(vector<pthread_t> *ptids, int threadnum = 3) { for (int i = 0; i < threadnum; i++) { char *name = new char[64]; snprintf(name, 64, "slaver-%d", i + 1); pthread_t tid; int n = pthread_create(&tid, nullptr, SlaverCore, (void *)name); if (n == 0) { cout << "create "<<name<<" Thread success" << endl; ptids->emplace_back(tid); } } } void WaitThread(vector<pthread_t> *ptids){ for(auto &tid:*ptids){ pthread_join(tid,nullptr); } } int main() { vector<pthread_t> tids; StartMaster(&tids); StartSlaver(&tids, 5); WaitThread(&tids); return 0; }
2.1.3生产者消费模型
2.1.3.1为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
形象的理解就是供货商-超市-顾客对应生产者-阻塞队列-消费者。超市是临界资源,供货商和顾客就是多个线程。
超市也可以理解为 临时存储数据的"内存空间" ,这个内存空间一般就是某种数据结构(比如上面的队列),是数据"交易"的场所。数据是什么?就是超市里的商品。
很显然,生产者模型的本质其实是处理并发时数据传递的问题。那么在这个模型中,会出现3种并发的问题。
生产者vs生产者:是互斥关系,简单的理解就是同行是冤家,同类商品供货商与供货商之间是竞争关系,甚至不同类商品的供货商与供货商之间也是竞争关系。当然同步也是可以的,这里暂且不说
消费者vs消费者:互斥,当商品数量太少,再比如演唱会门票,粉丝之间就是竞争关系,抢票。当然同步也是可以的,这里暂且不说
生产者vs消费者:互斥关系,生产者摆放商品的时候,消费者不能来打扰生产者,同样的,消费者正在选择商品的时候,生产者也不应该去摆放商品。
因此总结一下,就是3种关系(前面刚讲)、2种角色(生产者和消费者)、1个交易场所(比如超市,更严谨的就是某种数据结构对象),即321(方便记忆)。
2.1.3.2生产者消费者模型优点
解耦( 生产者添加数据和消费者读取数据相互不影响。在代码上,就是生产者和消费者并不直接相互调用,两者的代码发生变化时对对方都不产生影响**)
支持并发(** 如果没有缓冲区,消费者直接从生产者拿去数据时,就需要等待生产者产生数据,同样生产者也需要等待消费者消费数据。而在该模型下,生产者和消费者是两个独立并发的主体,不需要等待,也就是说生产者在生产,消费者在消费,这个过程是并发的,唯一的串行操作,就只有对阻塞队列进行操作的时候(将数据放到阻塞队列和从阻塞队列取数据),而且多个生产者/消费者之间本身也是并发的生产/消费数据**)
支持忙闲不均(** 在该模型下,缓冲区未放满时,生产者和消费者并不相互影响,所以不会产生占用CPU时间片的问题;而当缓冲区放满时,生产者就不在生产数据,同样消费者在缓冲区空时也不会再消费数据。使得两者的运行处于一种动态平衡的状态**)**
2.1.4基于BlockingQueue的生产者消费者模型
2.1.4.1 BlockingQueue
在多线程编程中阻塞队列(BlockingQueue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
2.1.4.2模拟阻塞队列
(里面的Start系列函数,还有一些问题,我也是写完了才发现的,可以看我最下面的环形队列中Init系统配合start函数的形式的多线程调用)
1.单生产+单消费(多生产者多消费者,在代码上没有区别,因为有互斥锁和同步,就不会乱)任务是纯数字
这里要说明一个关键问题,那就是虚假唤醒的问题,
什么是虚假唤醒 ?
线程可能在没有收到信号的情况下从pthread_cond_wait()中返回
这是POSIX标准允许的行为
可能由操作系统调度、信号处理等原因引起
一个具体例子:假设队列容量=5,当前有5个元素(已满):
生产者P1进入Enqueue(),发现队列满,调用pthread_cond_wait()等待
消费者C消费了1个元素,调用pthread_cond_signal()唤醒生产者
生产者P1被唤醒,但此时另一个生产者P2抢先获得锁,并成功插入一个元素
队列又变成满的(5个元素)
P1获取锁继续执行(用if会直接执行push,导致超过容量)
cpp#include "BlockQueue.hpp" #include "Thread.hpp" #include <vector> using namespace ThreadModule; int cnt = 10; void Consumer(BlockQueue<int>&bq) { while (true) { sleep(1); int data; bq.Pop(&data); cout << "Consumber consume data is : " <<data<< endl; } } void Productor(BlockQueue<int>&bq) { while (true) { bq.Enqueue(cnt); cout << "Productor product data is: " <<cnt++<< endl; } } void StartComm(std::vector<Thread<BlockQueue<int>>> *threads, int num, BlockQueue<int> &bq, func_t<BlockQueue<int>> func) { for (int i = 0; i < num; i++) { string name = "thread-" + std::to_string(i + 1); threads->emplace_back(func, bq, name); threads->back().Start(); } } void StartConsumer(std::vector<Thread<BlockQueue<int>>> *threads, int num, BlockQueue<int> &bq) { StartComm(threads, num,bq, Consumer); } void StartProductor(std::vector<Thread<BlockQueue<int>>> *threads, int num, BlockQueue<int> &bq) { StartComm(threads, num,bq, Productor); } void WaitAllThread(std::vector<Thread<BlockQueue<int>>> &threads) { for (auto &thread : threads) { thread.Join(); } } int main() { BlockQueue<int> *bq = new BlockQueue<int>(5); std::vector<Thread<BlockQueue<int>>> threads; StartConsumer(&threads, 5, *bq); StartProductor(&threads, 1, *bq); WaitAllThread(threads); return 0; }BlockQueue.hpp
cpp#ifndef __BLOCK_QUEUE_HPP__ #define __BLOCK_QUEUE_HPP__ #include <iostream> #include <string> #include <queue> #include <pthread.h> template <class T> class BlockQueue { public: BlockQueue(int cap) : _cap(cap) { _productor_wait_num=0; _consumer_wait_num=0; pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_product_cond, nullptr); pthread_cond_init(&_consumer_cond, nullptr); } void Enqueue(const T &in) { // 生产者用的接口 pthread_mutex_lock(&_mutex); while(isFull()){ //wait需要传锁,会把锁释放掉,因为要让消费者能够进入临界区从而让阻塞队列不再满 _productor_wait_num++;//这里不用担心,因为是持有锁的状态,所以会严格保证等待者的数量 pthread_cond_wait(&_product_cond,&_mutex); _productor_wait_num--; } //进行生产 // _block_queue.push(std::move(in)); _block_queue.push(in); //通知消费者消费,同下面生产者一样 if(_consumer_wait_num>0)(&_consumer_cond); pthread_mutex_unlock(&_mutex); } void Pop(T *out) { // 消费者用的接口 pthread_mutex_lock(&_mutex); while(isEmpty()){//这里之所以用while是因为虚假唤醒的原因,保证健壮性 _consumer_wait_num++; pthread_cond_wait(&_consumer_cond,&_mutex); _consumer_wait_num--; } //进行消费 *out=_block_queue.front(); _block_queue.pop(); //通知生产者生产,条件大于0,因为可能所以生产者都没有等待条件变量,都是在等待锁,条件一直满足,就没必要去通知 if(_productor_wait_num>0)pthread_cond_signal(&_product_cond); pthread_mutex_unlock(&_mutex); } ~BlockQueue() { pthread_cond_destroy(&_product_cond); pthread_cond_destroy(&_consumer_cond); pthread_mutex_destroy(&_mutex); } private: bool isFull(){ return _block_queue.size()==_cap; } bool isEmpty(){ return _block_queue.empty(); } private: std::queue<T> _block_queue; // 阻塞队列 int _cap; // 队列总上限 pthread_mutex_t _mutex; // 保护阻塞队列的锁 pthread_cond_t _product_cond; // 控制生产者的条件变量 pthread_cond_t _consumer_cond; // 控制消费者的条件变量 int _productor_wait_num;//等待的生产者数量 int _consumer_wait_num;//等待的消费者数量 }; #endifThread.hpp
cpp#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<functional> namespace ThreadModule{ template<class T> using func_t =std::function<void(T&)>; using std::cout; using std::cin; using std::endl; using std::string; template<class T> class Thread{ public: Thread(func_t<T> func,T&data,const string&name="default") :_func(func),_data(data),_threadname(name),_stop(true) {} void Excute(){ _func(_data); } static void *threadrun(void*arg)//注意,类成员函数有this指针,跟线程库的要求冲突了 //这里采取静态函数的方式,这样就没有this指针了,另外也可以放在类外面 { //注意,因为_func和_data都是成员函数,访问需要this指针 //但静态函数又没有this指针,那么就只能把this指针当参数传进threadrun函数。 //然后将方法的调用封装在Excute这个成员函数里 //这里就只需要用this指针直接调用Excute函数即可。 Thread<T> *self=static_cast<Thread*>(arg); self->Excute(); return nullptr; } bool Start(){ int n=pthread_create(&_tid,nullptr,threadrun,this); if(!n){ _stop=false; return true; } else{ return false; } } void Detach(){ if(!_stop){ pthread_detach(_tid); } } void Join(){ if(!_stop){ pthread_join(_tid,nullptr); } } void Stop(){_stop=true;} string name(){return _threadname;} ~Thread(){} private: pthread_t _tid; string _threadname; T &_data; func_t<T> _func; bool _stop; }; } #endif
2.任务不再是纯数字,增加额外的类形式。
cpp#include "BlockQueue.hpp" #include "Thread.hpp" #include <vector> #include "Task.hpp" #include <ctime> using namespace ThreadModule; using blockqueue_t = BlockQueue<Task>; // 方便改传参类型,using类似typedef class ThreadData{ private: blockqueue_t &bq; string who; };//这样线程就可以不放bq而是放td,可以传递的内容就会更多。我这里就不实现了。 void Consumer(blockqueue_t &bq) { while (true) { // 1.从blockqueue取下来任务 Task t; bq.Pop(&t); // 2.处理这个任务 t.Excute(); cout << "Consumber consume data is : " << t.ResultToString() << endl; } } void Productor(blockqueue_t &bq) { srand(time(nullptr) ^ pthread_self()); while (true) { sleep(1); // 1.获取任务 int a = rand() % 10 + 1; usleep(1234); int b = rand() % 10 + 1; Task t(a, b); // 2把获取的任务放入blockqueue里 bq.Enqueue(t); cout << "Productor product data is: " << t.DebugToString() << endl; } } void StartComm(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq, func_t<blockqueue_t> func) { for (int i = 0; i < num; i++) { string name = "thread-" + std::to_string(i + 1); threads->emplace_back(func, bq, name); threads->back().Start(); } } void StartConsumer(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq) { StartComm(threads, num, bq, Consumer); } void StartProductor(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq) { StartComm(threads, num, bq, Productor); } void WaitAllThread(std::vector<Thread<blockqueue_t>> &threads) { for (auto &thread : threads) { thread.Join(); } } int main() { blockqueue_t *bq = new blockqueue_t(5); std::vector<Thread<blockqueue_t>> threads; StartConsumer(&threads, 5, *bq); StartProductor(&threads, 1, *bq); WaitAllThread(threads); return 0; }BlockQueue.hpp
cpp#ifndef __BLOCK_QUEUE_HPP__ #define __BLOCK_QUEUE_HPP__ #include <iostream> #include <string> #include <queue> #include <pthread.h> template <class T> class BlockQueue { public: BlockQueue(int cap) : _cap(cap) { _productor_wait_num=0; _consumer_wait_num=0; pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_product_cond, nullptr); pthread_cond_init(&_consumer_cond, nullptr); } void Enqueue(const T &in) { // 生产者用的接口 pthread_mutex_lock(&_mutex); while(isFull()){ //wait需要传锁,会把锁释放掉,因为要让消费者能够进入临界区从而让阻塞队列不再满 _productor_wait_num++;//这里不用担心,因为是持有锁的状态,所以会严格保证等待者的数量 pthread_cond_wait(&_product_cond,&_mutex); _productor_wait_num--; } //进行生产 // _block_queue.push(std::move(in)); _block_queue.push(in); //通知消费者消费,同下面生产者一样 if(_consumer_wait_num>0)pthread_cond_signal(&_consumer_cond); pthread_mutex_unlock(&_mutex); } void Pop(T *out) { // 消费者用的接口 pthread_mutex_lock(&_mutex); while(isEmpty()){//这里之所以用while是因为虚假唤醒的原因,保证健壮性 _consumer_wait_num++; pthread_cond_wait(&_consumer_cond,&_mutex); _consumer_wait_num--; } //进行消费 *out=_block_queue.front(); _block_queue.pop(); //通知生产者生产,条件大于0,因为可能所以生产者都没有等待条件变量,都是在等待锁,条件一直满足,就没必要去通知 if(_productor_wait_num>0)pthread_cond_signal(&_product_cond); pthread_mutex_unlock(&_mutex); } ~BlockQueue() { pthread_cond_destroy(&_product_cond); pthread_cond_destroy(&_consumer_cond); pthread_mutex_destroy(&_mutex); } private: bool isFull(){ return _block_queue.size()==_cap; } bool isEmpty(){ return _block_queue.empty(); } private: std::queue<T> _block_queue; // 阻塞队列 int _cap; // 队列总上限 pthread_mutex_t _mutex; // 保护阻塞队列的锁 pthread_cond_t _product_cond; // 控制生产者的条件变量 pthread_cond_t _consumer_cond; // 控制消费者的条件变量 int _productor_wait_num;//等待的生产者数量 int _consumer_wait_num;//等待的消费者数量 }; #endifThread.hpp没有变化.
Task.hpp
cpp#pragma once #include <iostream> #include <string> class Task { public: Task() {} Task(int a, int b) : _a(a), _b(b), _result(0) { } void Excute() { _result = _a + _b; } std::string ResultToString() { return std::to_string(_a) + "+" + std::to_string(_b) + "=" + std::to_string(_result); } std::string DebugToString() { return std::to_string(_a) + "+" + std::to_string(_b) + "=?"; } ~Task() {} private: int _a, _b; int _result; };
3.任务也不局限于自定义类,我们完全可以把函数当做任务传过去把函数用functional包装起来,就可以放到容器里了。
cpp#include "BlockQueue.hpp" #include "Thread.hpp" #include <vector> #include "Task.hpp" #include <ctime> using namespace ThreadModule; using blockqueue_t = BlockQueue<Task>; // 方便改传参类型,using类似typedef void Print(){ cout<<"wdawdadwa"<<endl; } void Consumer(blockqueue_t &bq) { while (true) { // 1.从blockqueue取下来任务 Task t; bq.Pop(&t); // 2.处理这个任务 t(); //cout << "Consumber consume data is : " << t.ResultToString() << endl; } } void Productor(blockqueue_t &bq) { srand(time(nullptr) ^ pthread_self()); while (true) { sleep(1); // 1.获取任务 // int a = rand() % 10 + 1; // usleep(1234); // int b = rand() % 10 + 1; // Task t(a, b); // 2把获取的任务放入blockqueue里 bq.Enqueue(Print); //cout << "Productor product data is: " << t.DebugToString() << endl; } } void StartComm(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq, func_t<blockqueue_t> func) { for (int i = 0; i < num; i++) { string name = "thread-" + std::to_string(i + 1); threads->emplace_back(func, bq, name); threads->back().Start(); } } void StartConsumer(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq) { StartComm(threads, num, bq, Consumer); } void StartProductor(std::vector<Thread<blockqueue_t>> *threads, int num, blockqueue_t &bq) { StartComm(threads, num, bq, Productor); } void WaitAllThread(std::vector<Thread<blockqueue_t>> &threads) { for (auto &thread : threads) { thread.Join(); } } int main() { blockqueue_t *bq = new blockqueue_t(5); std::vector<Thread<blockqueue_t>> threads; StartConsumer(&threads, 5, *bq); StartProductor(&threads, 1, *bq); WaitAllThread(threads); return 0; }Task.hpp
cpp#pragma once #include <iostream> #include <string> #include<functional> using Task=std::function<void()>;
2.1.5为什么pthread_cond_wait需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
注意,如果我们的代码是先上锁,发现条件不满足,解锁,再等待条件变量,这是错误的,因为解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过。
由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,并且条件满足,发送了信号,那么pthread_cond_wait将错过这个信
号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子
操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t *mutex);进入该函数后,会去看条件量是否等于0?等于,就把互斥量变成1,直到cond_wait返回,把条件量改成1,把互斥量恢复成原样(0)。
因此一般条件变量是这样使用的:
等待条件
cpppthread_mutex_lock(&mutex) ; while (条件为假) pthread_cond_wait(cond, mutex); 修改条件 pthread_mutex_unlock(&mutex) ;给条件变量发生信号:
cpppthread_mutex_lock(&mutex) ; 设置条件为真 pthread_cond_signal(cond) ; pthread_mutex_unlock(&mutex) ;
2.2POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。但POSIX可以用于线程间同步。
信号量本质是计数器。
不难发现,我们前面的那个生产者消费者模型所用的阻塞队列,是以整体被使用的,不能既插入又删除,所以我们用的是互斥锁,而二元信号量(即信号量为1),可以用作互斥锁。
另外,互斥锁不能做到同步,要搭配条件变量,也就是说需要内部判断条件是否满足。
而信号量不同,信号量是衡量资源数目的,只要申请成功(申请本身就是在做判断了),就一定有队友的资源提供给你,有效减少内部判断。
初始化信号量
cpp#include <semaphore.h>(注意,这个头文件是pthread库提供的) int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: pshared:0表示线程间共享,非零表示进程间共享(父子进程,如果是不相关进程,还是用system V信号量) value:信号量初始值 成功为0,失败为-1,错误码被设置。
销毁信号量
cppint sem_destroy(sem_t *sem);
等待信号量
cpp功能:等待信号量,会将信号量的值减1 int sem_wait(sem_t *sem); //P() 信号量大于0,信号量--,继续执行代码。 信号量小于等于0,阻塞等待。 trywait和timewait,一个是失败不会阻塞等待,一个是允许阻塞等待一段时间。
发布信号量
cpp功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//v()
2.2.1基于环形队列的生产消费模型
现在基于固定大小的环形队列重写模型代码
环形队列我C/C++的文章里有提及,这里只是简单讲下细节。
我们用数据来实现环形队列,我们先考虑单生产者和单消费者的情况,这两者各一个指针(P,C),其中当P,C相同时,只有2种情况,要么队列满要么队列空,也就是说,如果P,C不相同,一定不为空&&一定不为满。(条件上,可以互相推出对方)。这意味着,生产者和消费者可以做到并发。
当队列为空,访问临界资源,必须先让生产者跑,满时,访问临界资源,让消费者先跑。
因此生产者不能把消费者套一个圈,消费者不能超过生产者。
生产者的代码,简单可以概括为p(space),v(data),消费者是p(data),v(space)其中space和data都是信号量,初始值为队列空间和0(代表数据数量)
pv就是pv操作。p是--,v是++。
这样的话,就可以保证,如果队列为空,哪怕消费者先跑,也会因为data为0,从而阻塞,只有生产者可以先跑。如果队列为满(信号量为0,10),也可以保证,生产者不能继续跑,只有消费者可以继续跑。如果不满的话,非空,只要data为0,那么消费者就不可能跑,只能让生产者跑,保证了生产者一直在消费者前头,而且在这种情况下,生产者可以生产自己的,消费者可以消费自己的,可以并发。
这也保证了前面所说的"当队列为空,访问临界资源,必须先让生产者跑,满时,访问临界资源,让消费者先跑。因此生产者不能把消费者套一个圈,消费者不能超过生产者。"
单生产单消费
cpp#include "RingQueue.hpp" #include "Thread.hpp" #include <vector> #include <ctime> using namespace ThreadModule; using ringqueue_t = RingQueue<int>; // 方便改传参类型,using类似typedef void Consumer(ringqueue_t &rq) { while (true) { sleep(3); int data=0; rq.Pop(&data); cout<<"Consumer : "<<data<<endl; } } void Productor(ringqueue_t &rq) { srand(time(nullptr) ^ pthread_self()); int cnt=10; while (true) { rq.Enqueue(cnt); cout<<"Productor : "<<cnt--<<endl; } } void StartComm(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq, func_t<ringqueue_t> func) { for (int i = 0; i < num; i++) { string name = "thread-" + std::to_string(i + 1); threads->emplace_back(func, rq, name); threads->back().Start(); } } void StartConsumer(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq) { StartComm(threads, num, rq, Consumer); } void StartProductor(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq) { StartComm(threads, num, rq, Productor); } void WaitAllThread(std::vector<Thread<ringqueue_t>> &threads) { for (auto &thread : threads) { thread.Join(); } } int main() { ringqueue_t *rq = new ringqueue_t(10); std::vector<Thread<ringqueue_t>> threads; StartProductor(&threads, 1, *rq); StartConsumer(&threads,1, *rq); WaitAllThread(threads); return 0; }RingQueue.hpp
cpp#pragma once #include<iostream> #include<string> #include<vector> #include<semaphore.h> template<class T> class RingQueue{ public: RingQueue(int cap):_cap(cap),_ring_queue(cap),_productor_index(0),_consumer_index(0) { sem_init(&_space_sem,0,_cap); sem_init(&_data_sem,0,0); } void Enqueue(const T&in){ //生产行为 P(_space_sem); //走到这里,一定有空间 _ring_queue[_productor_index++]=in;//生产 _productor_index%=_cap; V(_data_sem); } void Pop(T*out){ //消费行为 P(_data_sem); *out=_ring_queue[_consumer_index++];//消费 _consumer_index%=_cap; V(_space_sem); } ~RingQueue() { sem_destroy(&_space_sem); sem_destroy(&_data_sem); } private: void P(sem_t &sem){ sem_wait(&sem); } void V(sem_t &sem){ sem_post(&sem); } private: std::vector<T> _ring_queue;//环形队列 int _cap;//容量 int _productor_index;//生产者下标 int _consumer_index;//消费者下标 sem_t _space_sem;//剩余空间信号量-生产者 sem_t _data_sem;//剩余数据信号量-消费者 };
多生产多消费前面的单生产单消费,只处理了生产者和消费者之间的互斥和同步。
但生产者之间、消费者之间的互斥,并没有处理。
这时候解决方案就是加锁,锁的数量(1把也可以,但是就完全放弃了我们并发的初衷)为2把。生产者之间一把,消费者之间一把。
这里我改了下多线程调用的逻辑,上面的有一些问题。主函数还在插入,但是已经start的线程已经依靠back().Start(),去执行Consumer或者Productor函数了,这时候back对象已经发生了变化,内部指针指向了新的元素,而我们封装的线程类里,内部指针指向了新元素,这就导致最后真正执行Consumer函数的时候,传递的参数是新的线程类,而新的线程类,比如名字这个属性,可能已经初始化,但是没有值,这就导致最后输出的时候出现名字为空的现象。
因此,我这里是选择全部初始化一遍,然后再启动,这样就不会出现问题了。
cpp#include "RingQueue.hpp" #include "Thread.hpp" #include"Task.hpp" #include <vector> #include <ctime> using namespace ThreadModule; using ringqueue_t = RingQueue<Task>; // 方便改传参类型,using类似typedef void Consumer(ringqueue_t &rq,const string&name) { while (true) { sleep(2); //消费任务 Task t; rq.Pop(&t); cout<<"Consumer handler task : "<<name<<"["<<name<<"]"<<endl; //处理任务 t(); } } void Productor(ringqueue_t &rq,const string&name) { srand(time(nullptr) ^ pthread_self()); while (true) { //获取任务 //生产任务 rq.Enqueue(Download); cout<<"Productor : "<<"["<<name<<"]"<<endl; } } void InitComm(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq, func_t<ringqueue_t> func,const string &who) { for (int i = 0; i < num; i++) { string name = "thread-" + std::to_string(i + 1)+"-"+who; threads->emplace_back(func, rq, name); } } void InitConsumer(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq) { InitComm(threads, num, rq, Consumer,"consumer"); } void InitProductor(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq) { InitComm(threads, num, rq, Productor,"product"); } void WaitAllThread(std::vector<Thread<ringqueue_t>> &threads) { for (auto &thread : threads) { thread.Join(); } } void StartAll(std::vector<Thread<ringqueue_t>> &threads) { for(auto &thread:threads){ thread.Start(); } } int main() { ringqueue_t *rq = new ringqueue_t(10); std::vector<Thread<ringqueue_t>> threads; InitProductor(&threads, 3, *rq); InitConsumer(&threads,2, *rq); StartAll(threads); WaitAllThread(threads); return 0; }RingQueue.hpp
cpp#pragma once #include <iostream> #include <string> #include <vector> #include <semaphore.h> #include <pthread.h> template <class T> class RingQueue { public: RingQueue(int cap) : _cap(cap), _ring_queue(cap), _productor_index(0), _consumer_index(0) { sem_init(&_space_sem, 0, _cap); sem_init(&_data_sem, 0, 0); pthread_mutex_init(&_productor_mutex, nullptr); pthread_mutex_init(&_consumer_mutex, nullptr); } void Enqueue(const T &in) { //注意,可以在外面套锁,但反正都是等,不如先申请了信号量再等。 //等拿到锁,马上就开始生成,效率会更高,而不是拿到锁之后还要先去申请信号量。 // 生产行为 P(_space_sem); // 走到这里,一定有空间 Lock(_productor_mutex); _ring_queue[_productor_index++] = in; // 生产 _productor_index %= _cap; Unlock(_productor_mutex); V(_data_sem); } void Pop(T *out) { // 消费行为 P(_data_sem); Lock(_consumer_mutex); *out = _ring_queue[_consumer_index++]; // 消费 _consumer_index %= _cap; Unlock(_consumer_mutex); V(_space_sem); } ~RingQueue() { sem_destroy(&_space_sem); sem_destroy(&_data_sem); pthread_mutex_destroy(&_productor_mutex); pthread_mutex_destroy(&_consumer_mutex); } private: void P(sem_t &sem) { sem_wait(&sem); } void V(sem_t &sem) { sem_post(&sem); } void Lock(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); } void Unlock(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); } private: std::vector<T> _ring_queue; // 环形队列 int _cap; // 容量 int _productor_index; // 生产者下标 int _consumer_index; // 消费者下标 sem_t _space_sem; // 剩余空间信号量-生产者 sem_t _data_sem; // 剩余数据信号量-消费者 pthread_mutex_t _productor_mutex; // 生产者之间的互斥关系 pthread_mutex_t _consumer_mutex; // 消费者之间的互斥关系 };Thread.hpp
cpp#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<functional> namespace ThreadModule{ template<class T> using func_t =std::function<void(T&,const std::string &name)>; using std::cout; using std::cin; using std::endl; using std::string; template<class T> class Thread{ public: Thread(func_t<T> func,T&data,const string&name="default") :_func(func),_data(data),_threadname(name),_stop(true) {} void Excute(){ _func(_data,_threadname); } static void *threadrun(void*arg)//注意,类成员函数有this指针,跟线程库的要求冲突了 //这里采取静态函数的方式,这样就没有this指针了,另外也可以放在类外面 { //注意,因为_func和_data都是成员函数,访问需要this指针 //但静态函数又没有this指针,那么就只能把this指针当参数传进threadrun函数。 //然后将方法的调用封装在Excute这个成员函数里 //这里就只需要用this指针直接调用Excute函数即可。 Thread<T> *self=static_cast<Thread*>(arg); self->Excute(); return nullptr; } bool Start(){ int n=pthread_create(&_tid,nullptr,threadrun,this); if(!n){ _stop=false; return true; } else{ return false; } } void Detach(){ if(!_stop){ pthread_detach(_tid); } } void Join(){ if(!_stop){ pthread_join(_tid,nullptr); } } void Stop(){_stop=true;} string name(){return _threadname;} ~Thread(){} private: pthread_t _tid; string _threadname; T &_data; func_t<T> _func; bool _stop; }; } #endifTask.hpp
cpp#pragma once #include<iostream> #include<functional> using Task =std::function<void()>; void Download(){ std::cout<<"this is a download task"<<std::endl; }
3.线程池
3.1日志与策略模式
什么是设计模式
针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是设计模式。
日志认识
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志格式以下几个指标是必须得有的
时间戳
日志等级
日志内容
以下几个指标是可选的
文件名行号
进程,线程相关id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,这里我们依旧采用自定义日志的方式。这里我们采用设计模式-策略模式来进行日志的设计。
我们想要的日志格式:
可读性好的时间\]\[日志等级\]\[进程pid\]\[打印对应日志的文件名\]\[行号\] - 消息内容,支持可变参数 具体的日志,我会在下面的线程池设计中体现。
时间获取
因为用到了日志,第一个难关,时间打印,我们这边使用了time.h头文件提供的函数。
time_t其实就是整数。
cppsetenv("TZ", "EST5EDT", 1); // 或 "Asia/Shanghai" 等 tzset(); // 使时区设置生效注意,time获取的是绝对的时间戳,但时间戳转换成的时间,在不同时区是不同的,比如我的云服务器默认是东八区,所以时间跟我windows电脑的时间是一致的,但如果通过上面的函数,改变时区,就可以让同样的时间戳变成不同时区的时间,比如美国东部时间什么的。
获取当前文件和行数
另外,**第二个难关,如何获取当前执行的代码的文件名和行数呢?**C语言已经有预处理符了,FILE__和__LINE。直接用就行了。
如何使用可变参数来传参
第三个难关,如何使用可变参数(c++的可以参考我c++11的文章),我们这里使用c语言的函数。
cpp//我们先假定传递进来的都是整数,c语言的可变参数要求一定有个确定的类型,且必须在左边 //因为参数是从右往左依次入栈的,所以依靠这个确定的类型,就可以知道可变参数的地址 void Test(int num,...){ va_list arg;//这个类型,本质就是指针 va_start(arg,num);//这个函数是个宏,用num这个类型的地址,来初始化arg,从而来指向可变部分 while(num--){ //这个函数作用,就是从arg开始,以第二个参数的类型,每次提取一个数据返回。 //提取完,arg+=sizeof(第二个参数),这样就能把所有可变部分参数都提取完。 //当前前提是这里的num是规范的(表示了有多少个可变参数)。 int data=va_arg(arg,int); std::cout<<"data: "<<data<<std::endl; } va_end(arg);//这个函数是用来arg=NULL,清空的。 }上面的写法比较粗糙,而且比较繁琐,下面会更加简便。
这个也是printf底层的设计,为什么占位符要指定类型? 因为要提取的时候需要知道是什么类型,为什么可变参数部分数量和顺序要匹配?,因为提取的时候是按前面字符串里的占位符,一个个匹配提取出来的。
cppvoid LogMessage(std::string filename,int line,int level, const char *format, ...) { std::string levelstr = LevelToString(level); std::string timestr=GetTimeString(); pid_t selfid=getpid(); char buffer[1024]; va_list arg; va_start(arg,format);//给指针,给格式化的字符串,然后就会按照格式化要求初始化可变部分。 //按照format的格式,从arg开始的可变部分,传进buffer字符数组里。 //这个format,参考printf的format即可。 vsnprintf(buffer,sizeof(buffer),format,arg); va_end(arg); std::cout<<"["<<levelstr<<"]" <<"["<<timestr<<"]" <<"[pid:"<<selfid<<"]" <<"[file:"<<filename<<"]" <<"[line:"<<line<<"]" <<"---"<<buffer<<std::endl; //std::cout<<levelstr<<" pid:"<<selfid<<" : "<<timestr<<" : "<<filename<<":"<<line<<" :"<<buffer<<std::endl; } //因为文件名和行数,不需要我们自己找,为了方便,我们可以定义一个宏来替换 //C99及以上,乃至c++,支持宏里带可变参数的。 //__VA_ARGS__代表了可变参数部分。 //##是为了防止,如果传参的时候压根就没有可变参数部分,##意味着从__VA_ARGS__部分的内容都会被忽视。 //为了防止宏替换导致的隐患,加个do while,这样替换就不会影响其他代码。 #define LOG(level,format, ...) do{LogMessage(__FILE__,__LINE__,level,format, ##__VA_ARGS__);}while(0)
cppLogMessage(__FILE__,__LINE__,ERROR,"hello : %s,now is %d","world",4); LOG(DEBUG,"hello : %s,now is %d","world",4);//LogMessage(__FILE__,__LINE__,level,format, __VA_ARGS__) LOG(INFO,"hello world");
3.2线程池设计
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误
线程池的种类
1.创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口
2.浮动线程池,其他同上
此处,我们选择固定线程个数的线程池。
ThreadPool.hpp
cpp#pragma once #include <iostream> #include <vector> #include <queue> #include <pthread.h> #include "Thread.hpp" #include"Log.hpp" using namespace ThreadModule; const static int gdefaultthreadnum = 3; template <class T> class ThreadPool { public: ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); LOG(INFO,"ThreadPool Construct()"); } // 注意,因为成员函数有this指针,参数不一致,如果不bind,可以用static。 // 用bind的话,就可以让类成员方法成为另一个类的回调方法,方便我们继续类级别的互相调用。 void HandlerTask(const std::string &name) { LOG(INFO,"%s is running...",name.c_str()); while (true) { // 1.保证队列安全 LockQueue(); // 2.队列中不一定有数据 while (_task_queue.empty() && _isrunning) { ThreadSleep(); } // 2.1如果线程池已经退出&&任务队列为空 if (_task_queue.empty() && !_isrunning) { UnlockQueue(); break; } // 2.2如果(线程池不退出||线程池退出)&&任务不为空 继续执行,直到任务处理完且线程池退出 // 3.一定有任务,先获取任务 T t = _task_queue.front(); _task_queue.pop(); UnlockQueue(); LOG(DEBUG,"%s get a task",name.c_str()); // 4.处理任务,因为任务已经被获取,属于该线程独占,所以不要放在锁里处理 t(); LOG(DEBUG,"%s handler a task, result is: %s",name.c_str(),t.ResultToString().c_str()); } } void InitThreadPool() { for (int num = 0; num < _threadnum; num++) { std::string name = "thread-" + std::to_string(num + 1); _threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name); LOG(INFO,"init thread %s done",name.c_str()); } LockQueue(); // 保护 //必须在初始化这设置,如果在Start那设置, //因为还没初始化,一些已经Start()的线程就因为false的原因退出了 _isrunning = true; UnlockQueue(); } void Stop() { LockQueue(); // 保护 _isrunning = false; ThreadWakeUpAll(); UnlockQueue(); } void Start() { for (auto &thread : _threads) { thread.Start(); } } void Wait() { for (auto &thread : _threads) { thread.Join(); LOG(INFO,"%s is quit...",thread.name().c_str()); } } bool Enqueue(const T &in) { bool ret = false; LockQueue(); if (_isrunning) // 线程池在启动状态才允许入队列 { _task_queue.push(in); if (_waitnum > 0) // 如果有线程在等待任务,唤醒线程 { ThreadWakeUp(); } LOG(DEBUG,"enqueue task success"); ret = true; } UnlockQueue(); return ret; } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } private: void LockQueue() { pthread_mutex_lock(&_mutex); } void UnlockQueue() { pthread_mutex_unlock(&_mutex); } void ThreadSleep() { _waitnum++; pthread_cond_wait(&_cond, &_mutex); _waitnum--; } void ThreadWakeUp() { pthread_cond_signal(&_cond); } void ThreadWakeUpAll() { pthread_cond_broadcast(&_cond); _waitnum = 0; } private: int _threadnum; std::vector<Thread> _threads; std::queue<T> _task_queue; pthread_mutex_t _mutex; pthread_cond_t _cond; int _waitnum; bool _isrunning; };
cpp#include "ThreadPool.hpp" #include <iostream> #include <string> #include "Task.hpp" #include <memory> #include<ctime> int main() { srand(time(nullptr)^getpid()^pthread_self()); EnableLogScreen(); // EnableLogFile(); std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>(); // c++14新特性 tp->InitThreadPool(); tp->Start(); int tasknum = 10; while (tasknum--) { int a=rand()%10+1; usleep(1234); int b=rand()%5+1; Task t(a,b); LOG(INFO,"main thread push task: %s",t.DebugToString().c_str()); tp->Enqueue(t); sleep(1); } tp->Stop(); tp->Wait(); return 0; }Task.hpp
cpp#pragma once #include <iostream> #include <string> #include<functional> class Task { public: Task() {} Task(int a, int b) : _a(a), _b(b), _result(0) { } void Excute() { _result = _a + _b; } std::string ResultToString() { return std::to_string(_a) + "+" + std::to_string(_b) + "=" + std::to_string(_result); } std::string DebugToString() { return std::to_string(_a) + "+" + std::to_string(_b) + "=?"; } void operator()(){//重载圆括号,统一调用。 Excute(); } ~Task() {} private: int _a, _b; int _result; };Log.hpp
cpp#pragma once #include <iostream> #include <cstdio> #include <string> #include <ctime> #include <sys/types.h> #include <unistd.h> #include <cstdarg> #include <pthread.h> #include <fstream> #include "LockGuard.hpp" bool gIsSave = false; // 决定日志消息是否要保存 const std::string logpath = "log.txt"; enum Level { DEBUG = 0, INFO, WARNING, ERROR, FATAL }; void SaveFile(const std::string &filename, const std::string &message) { std::ofstream fout(filename, std::ios::app); // app是表示以追加方式写入 if (!fout.is_open()) { // 如果文件没有打开成功 return; } fout << message; // fout.write(message); fout.close(); } std::string LevelToString(int level) { switch (level) { case DEBUG: return "Debug"; case INFO: return "Info"; case WARNING: return "Warning"; case ERROR: return "Error"; case FATAL: return "Fatal"; default: return "Unknown"; } } std::string GetTimeString() { // time就是返回现在的时间戳,但nullptr的话,只有返回值 // 但也可以自己定一个time_t变量,地址传进去,这样最后返回值和变量都会存储当前时间戳 time_t cur_time = time(nullptr); struct tm *format_time = localtime(&cur_time); if (format_time == nullptr) return "None"; char time_buffer[1024]; snprintf(time_buffer, sizeof(time_buffer), "%04d-%02d-%02d %02d-%02d-%02d", format_time->tm_year + 1900, format_time->tm_mon + 1, format_time->tm_mday, format_time->tm_hour, format_time->tm_min, format_time->tm_sec); return time_buffer; } pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...) { std::string levelstr = LevelToString(level); std::string timestr = GetTimeString(); pid_t selfid = getpid(); char buffer[1024]; va_list arg; va_start(arg, format); // 给指针,给格式化的字符串,然后就会按照格式化要求初始化可变部分。 // 按照format的格式,从arg开始的可变部分,传进buffer字符数组里。 // 这个format,参考printf的format即可。 vsnprintf(buffer, sizeof(buffer), format, arg); va_end(arg); std::string message = "[" + levelstr + "]" + "[" + timestr + "]" + "[pid:" + std::to_string(selfid) + "]" + "[file:" + filename + "]" + "[line:" + std::to_string(line) + "]" + "---" + buffer + "\n"; LockGuard lockguard(&lock); // 因为终端本身也是一种共享资源,打印也需要保护 if (!issave) { std::cout << message; } else { // 日志内容写入文件(更复杂的可以按不同日志等级写入不同文件) SaveFile(logpath, message); } } // 因为文件名和行数,不需要我们自己找,为了方便,我们可以定义一个宏来替换 // C99及以上,乃至c++,支持宏里带可变参数的。 //__VA_ARGS__代表了可变参数部分。 // ##是为了防止,如果传参的时候压根就没有可变参数部分,##意味着从__VA_ARGS__部分的内容都会被忽视。 // 为了防止宏替换导致的隐患,加个do while,这样替换就不会影响其他代码。 #define LOG(level, format, ...) \ do \ { \ LogMessage(__FILE__, __LINE__, gIsSave, level, format, ##__VA_ARGS__); \ } while (0) #define EnableLogFile() \ do \ { \ gIsSave = true; \ } while (0) #define EnableLogScreen() \ do \ { \ gIsSave = false; \ } while (0)Thread.hpp
cpp#ifndef __THREAD_HPP__ #define __THREAD_HPP__ #include<iostream> #include<string> #include<unistd.h> #include<pthread.h> #include<functional> namespace ThreadModule{ using func_t =std::function<void(const std::string&)>; class Thread{ public: Thread(func_t func,const std::string&name="default") :_func(func),_threadname(name),_stop(true) {} void Excute(){ _func(_threadname); } static void *threadrun(void*arg) { Thread *self=static_cast<Thread*>(arg); self->Excute(); return nullptr; } bool Start(){ int n=pthread_create(&_tid,nullptr,threadrun,this); if(!n){ _stop=false; return true; } else{ return false; } } void Detach(){ if(!_stop){ pthread_detach(_tid); } } void Join(){ if(!_stop){ pthread_join(_tid,nullptr); } } void Stop(){_stop=true;} std::string name(){return _threadname;} ~Thread(){} private: pthread_t _tid; std::string _threadname; func_t _func; bool _stop; }; } #endifLockGuard.hpp
cpp#ifndef __LOCK_GUARD_HPP__ #define __LOCK_GUARD_HPP__ #include<iostream> #include<pthread.h> class LockGuard{ public: LockGuard(pthread_mutex_t *mutex):_mutex(mutex){ pthread_mutex_lock(_mutex); } ~LockGuard(){ pthread_mutex_unlock(_mutex); } private: pthread_mutex_t *_mutex; }; #endif
4.线程安全和重入问题
4.1概念
线程安全 :就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
重入 :同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。学到现在,其实我们已经能理解重入其实可以分为两种情况
多线程重入函数
信号导致一个执行流重复进入函数
4.2情况分类
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/0库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些
线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4.3总结
可重入与线程安全联系
函数是可重入的,那就是线程安全的(核心 )
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个函数在持有锁的情况下再次调用自身(递归调用),则会产生死锁,因此是不可重入的。
注意:
如果不考虑信号导致一个执行流重复进入函数这种重入情况,线程安全和重入在安全角
度不做区分
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
可重入描述的是一个函数是否能被重复进入,表示的是函数的特点
5.常见锁概念
5.1死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
比较简单的单锁出现死锁的情况,就是多次申请同一个锁,且不释放锁。那么持有锁的线程因为二次申请,但锁被占用,自己带着锁也跑去阻塞了,最后就没人释放锁了。
为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问
申请一把锁是原子的,但是申请两把锁就不一定了
造成的结果就是
5.2死锁的4个必要条件
互斥条件 :一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
5.3避免死锁
破坏死锁的四个必要条件
破坏互斥条件: 不用锁(有些公共资源不会被修改,只是会被读取,这时候就没必要加锁)**破坏请求与保持条件:**像c语言就提供了trylock接口,这个接口申请锁失败后不会阻塞而是马上返回一个int值,通过返回值确定是否失败,失败的话可以把自己占有的锁也释放掉,这样互相谦让之后,这个条件就被破坏了。
**破坏不剥夺条件:**反过来思考,那就是允许剥夺,比如不同线程间设置优先级,优先级高的可以剥夺优先级低的,剥夺的操作也是有实现可能的,因为解锁的本质是把锁置1,每个线程都可以这么做的,理论上可以不经占有锁的线程的同意,强行解锁。
破坏循环等待条件问题:资源一次性分配,使用超时机制、加锁顺序一致、避免锁未释放的场景
5.4避免死锁算法
死锁检测和银行家算法,核心就是检测队列是否处于安全状态。这里不多说,教材也很详细了,我们这里模拟也不现实,有兴趣的可以去了解下。
6.STL,智能指针和线程安全
6.1STL中的容器是否是线程安全的?
不是.
原因是,STL的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响.
而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶).
因此STL默认不是线程安全.如果需要在多线程环境下使用,往往需要调用者自行保证线程安全.
6.2智能指针是否是线程安全的?
对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题.
对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题.但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr能够高效,原子的操作引用计数.
注意,我上面说的,是针对于智能指针,但不代表指针指向的内容是线程安全
因为智能指针使用的时候,经常是需要绑定stl容器的,所以只要知道,stl要加锁,智能指针也是要加锁的。
7.线程安全的单例模式
关于单例模式,我之前特殊类的设计这个文章已经讲了特殊类的设计-CSDN博客,可以先看那篇文章。这里主要是做一个线程安全的补充。
在前面线程池的基础上进行修改,只有ThreadPool.hpp和Main.cc有修改。
ThreadPool.hpp
cpp#pragma once #include <iostream> #include <vector> #include <queue> #include <pthread.h> #include "Thread.hpp" #include "LockGuard.hpp" #include "Log.hpp" using namespace ThreadModule; const static int gdefaultthreadnum = 5; template <class T> class ThreadPool { public: static ThreadPool<T> *GetInstance() { // 多线程情况下,下面的代码得加锁,否则会有并发问题,导致维持不了单例。 //另外,因为只有第一次需要创建对象, //双判断的方式,可以保证第二次以后,所有线程不用加锁,直接返回对象即可, //有效减少获取单例的加锁成本,而且保证线程安全。 if (_instance == nullptr) { LockGuard lockguard(&_lock); if (_instance == nullptr) { _instance = new ThreadPool<T>(); _instance->InitThreadPool(); _instance->Start(); LOG(DEBUG, "创建线程池单例"); } else { LOG(DEBUG, "获取线程池单例"); } } else { LOG(DEBUG, "获取线程池单例"); } return _instance; } void Stop() { LockQueue(); // 保护 _isrunning = false; ThreadWakeUpAll(); UnlockQueue(); } void Wait() { for (auto &thread : _threads) { thread.Join(); LOG(INFO, "%s is quit...", thread.name().c_str()); } } bool Enqueue(const T &in) { bool ret = false; LockQueue(); if (_isrunning) // 线程池在启动状态才允许入队列 { _task_queue.push(in); if (_waitnum > 0) // 如果有线程在等待任务,唤醒线程 { ThreadWakeUp(); } LOG(DEBUG, "enqueue task success"); ret = true; } UnlockQueue(); return ret; } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } private: // 单例下,start,init,handlertask函数都放进私有 void Start() { for (auto &thread : _threads) { thread.Start(); } } // 注意,因为成员函数有this指针,参数不一致,如果不bind,可以用static。 // 用bind的话,就可以让类成员方法成为另一个类的回调方法,方便我们继续类级别的互相调用。 void HandlerTask(const std::string &name) { LOG(INFO, "%s is running...", name.c_str()); while (true) { // 1.保证队列安全 LockQueue(); // 2.队列中不一定有数据 while (_task_queue.empty() && _isrunning) { ThreadSleep(); } // 2.1如果线程池已经退出&&任务队列为空 if (_task_queue.empty() && !_isrunning) { UnlockQueue(); break; } // 2.2如果(线程池不退出||线程池退出)&&任务不为空 继续执行,直到任务处理完且线程池退出 // 3.一定有任务,先获取任务 T t = _task_queue.front(); _task_queue.pop(); UnlockQueue(); LOG(DEBUG, "%s get a task", name.c_str()); // 4.处理任务,因为任务已经被获取,属于该线程独占,所以不要放在锁里处理 t(); LOG(DEBUG, "%s handler a task, result is: %s", name.c_str(), t.ResultToString().c_str()); } } void InitThreadPool() { for (int num = 0; num < _threadnum; num++) { std::string name = "thread-" + std::to_string(num + 1); _threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1), name); LOG(INFO, "init thread %s done", name.c_str()); } LockQueue(); // 保护 // 必须在初始化这设置,如果在Start那设置, // 因为还没初始化,一些已经Start()的线程就因为false的原因退出了 _isrunning = true; UnlockQueue(); } // 单例模式下,构造方法必须有且私有 ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond, nullptr); LOG(INFO, "ThreadPool Construct()"); } // 单例模式下,赋值和拷贝必须禁用 ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; ThreadPool(const ThreadPool<T> &) = delete; void LockQueue() { pthread_mutex_lock(&_mutex); } void UnlockQueue() { pthread_mutex_unlock(&_mutex); } void ThreadSleep() { _waitnum++; pthread_cond_wait(&_cond, &_mutex); _waitnum--; } void ThreadWakeUp() { pthread_cond_signal(&_cond); } void ThreadWakeUpAll() { pthread_cond_broadcast(&_cond); _waitnum = 0; } private: int _threadnum; std::vector<Thread> _threads; std::queue<T> _task_queue; pthread_mutex_t _mutex; pthread_cond_t _cond; int _waitnum; bool _isrunning; // 添加单例模式 static ThreadPool<T> *_instance; static pthread_mutex_t _lock; }; template <class T> ThreadPool<T> *ThreadPool<T>::_instance = nullptr; // 注意,静态和全局的锁,比其init,用宏来赋值会更好 template <class T> pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
cpp#include "ThreadPool.hpp" #include <iostream> #include <string> #include "Task.hpp" #include <memory> #include<ctime> int main(){ LOG(DEBUG,"程序已经加载"); sleep(3); ThreadPool<Task>::GetInstance(); sleep(2); ThreadPool<Task>::GetInstance(); sleep(2); ThreadPool<Task>::GetInstance(); sleep(2); ThreadPool<Task>::GetInstance(); sleep(2); ThreadPool<Task>::GetInstance(); sleep(2); ThreadPool<Task>::GetInstance()->Stop(); ThreadPool<Task>::GetInstance()->Wait(); return 0; }
8.其他常见的各种锁
悲观锁 :在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁 :每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。自旋锁,读写锁。
9.自旋锁
9.1概念
自旋锁是一种多线程同步机制,用于保护共享资源免受并发访问的影响。在多个线程尝试获取锁时,它们会持续自旋(即在一个循环中不断检查锁是否可用)而不是立即进入休眠状态等待锁的释放。这种机制减少了线程切换的开销,适用于短时间内锁的竞争情况。但是不合理的使用,可能会造成CPU的浪费。
9.2原理
自旋锁通常使用一个共享的标志位(如一个布尔值)来表示锁的状态。当标志位为true时,表示锁已被某个线程占用;当标志位为false时,表示锁可用。当一个线程尝试获取自旋锁时,它会不断检查标志位:
如果标志位为false,表示锁可用,线程将设置标志位为true,表示自己占用了锁,并进入临界区。
如果标志位为true(即锁已被其他线程占用),线程会在一个循环中不断自旋等待,直到锁被释放。
9.3优缺点
9.3.1优点
1.低延迟 :自旋锁适用于短时间内的锁竞争情况,因为它不会让线程进入休眠状态,从而避免了线程切换的开销,提高了锁操作的效率。
2.减少系统调度开销:等待锁的线程不会被阻塞,不需要上下文切换,从而减少了系统调度的开销。
9.3.2缺点
1.CPU资源浪费 :如果锁的持有时间较长,等待获取锁的线程会一直循环等待,导致CPU资源的浪费。
2.可能引起活锁:当多个线程同时自旋等待同一个锁时,如果没有适当的退避策略,可能会导致所有线程都在不断检查锁状态而无法进入临界区,形成活锁。
9.4使用场景
1.短暂等待的情况 :适用于锁被占用时间很短的场景,如多线程对共享数据进行简单的读写操作。
**2.多线程锁使用:**通常用于系统底层,同步多个CPU对共享资源的访问。
9.5接口
自旋锁的实现通常使用原子操作来保证操作的原子性,常用的软件实现方式是通过CAS(CompareAnd-Swap)指令实现。这里就不实现了。
Linux提供的自旋锁系统调用
cpp#include <pthread.h> int pthread_spin_lock(pthread_spinlock_t *lock) ; int pthread_spin_trylock(pthread_spinlock_t *lock) ; int pthread_spin_unlock(pthread_spinlock_t *lock); int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy(pthread_spinlock_t *lock) ;这些接口,基本跟前面说的互斥锁用法一样,自旋是在pthread_spin_lock内部自旋,对于我们用户或者调用接口的人来说,一旦线程没有抢到锁,都是卡在这个接口位置,只是区别在于,一个是在接口内部自旋循坏,一个是直接进入阻塞状态挂起。
注意事项
在使用自旋锁时,需要确保锁被释放的时间尽可能短,以避免CPU资源的浪费。
在多CPU环境下,自旋锁可能不如其他锁机制高效,因为它可能导致线程在不同的CPU上自旋等待。
另外,通常情况下I/O操作和网络操作都比较耗时,建议用之前的二元信号量和互斥锁。
结论
自旋锁是一种适用于短时间内锁竞争情况的同步机制 ,它通过减少线程切换的开销来提高锁操作的效率。然而,它也存在CPU资源浪费和可能引起活锁等缺点。在使用自旋锁时,需要根据具体的应用场景进行选择,并确保锁被释放的时间尽可能短。
10.读者写者问题和读写锁
10.1读写写者问题
举个简单的例子,你在视频底下发评论,其他人看你的评论。这本身就是一个读者写者的问题。
在这个问题中,读者众多,写者较少是最常见的情况。
更加专业点术语的说法,就是有线程向公共资源写入,其他线程从公共资源中读取数据。
类似前面说的生产者消费者模型,这里也有个321,也就是3种关系、2种角色(读者和写者)、一个交易场所(比如视频平台等)3种关系:
读者和读者 :没有关系!这也是与生产者消费者的本质区别,即读者不会拿走数据只是拷贝数据(所以不需要互斥,也不需要同步),而消费者却会拿走数据。
写者和写者:互斥关系,不然数据会产生二义性
读者和写者:互斥和同步,保证数据不产生二义性的同时,读者读完要让写者写,写者写完也要让读者读。
10.2读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢?
有,那就是读写锁。下面是读者优先的情况。
注意:写独占,读共享,读锁优先级高
读者优先(Reader-Preference)
在这种策略中,系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据),而不会优先考虑写者。这意味着当有读者正在读取时,新到达的读者会立即被允许进入读取区,而写者则会被阻塞,直到所有读者都离开读取区。读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限),特别是当读者频繁到达时。
对于饥饿问题,其实不需要太担心,当成一种特性就可以了 。第一计算机速度够快,写者总是有机会的,其次,读者访问高峰期,写者本身就不应该进行写入,在读者访问低谷期,写者进行写入才是比较合情合理的。
写者优先(Writer-Preference)在这种策略中,系统会优先考虑写者。当写者请求写入权限时,系统会尽快地让写者进入写入区,即使此时有读者正在读取。这通常意味着一旦有写者到达,所有后续的读者都会被阻塞,直到写者完成写入并离开写入区。写者优先策略可以减少写者等待的时间,但可能会导致读者饥饿(即读者长时间无法获得读取权限),特别是当写者频繁到达时。
10.3读写锁接口
伪代码,理解读写锁逻辑
cppint reader_count=0;//正在进行读操作的读者数量 pthread_mutex_t wlock;//写锁 pthread_mutex_t rlock;//读锁 reader: lock(&rlock);//先抢读锁,因为要修改共享资源reader_count if(reader_count==0){ lock(&wlock);//如果是第一个读者,那么要把写锁抢走 //如果这时有写者过来,就要阻塞等待,这样直到下面的解锁之后,写者才能不阻塞。 //如果此时写者已经抢了这个锁,正在进行写入,那么这个读者就会阻塞等待,直到写者释放写锁 // } ++reader_count; unlock(&rlock); //这里进行常规read操作 lock(&rlock); --reader_count; if(reader_count==0){ unlock(&wlock); //如果此时当前的读者线程是最后一个读者了,那么就释放写锁 //让写者可以进行写入。 //结合上面的写锁加锁,就保证了读者和写者的互斥同步关系 } writer: lock(&wlock);//抢写锁,保证读者和写者,写者和写者间的互斥关系。 //write写入操作 unlock(&wlock);
设置读写优先
cppint pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); pref 共有 3 种选择 PTHREAD_RWLOCK_PREFER_READER_NP(默认设置)读者优先,可能会导致写者饥饿情况 PTHREAD_RWLOCK_PREFER_WRITER_NP写者优先,目前有BUG,导致表现行为和PTHREAD_RWLOCK_PREFER_READER_NP一致 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP写者优先,但写者不能递归加锁
初始化
cppint pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
销毁
cppint pthread_rwlock_destroy(pthread_rwlock_t *rwlock) ;
加锁和解锁
cppint pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) ; int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) ; 注意,锁内部会记录当前线程是读锁还是写锁,解锁接口内部会自行判断, 所以我们调用者不需要关系,统一用unlock解锁即可。
例子
下面代码是AI生成的
cpp#include <iostream> #include <pthread.h> #include <unistd.h> #include <vector> #include <cstdlib> #include <ctime> // 共享资源 int shared_data = 8; // 读写锁 pthread_rwlock_t rwlock; // 读者线程函数 void *Reader(void *arg) { //sleep(1); // 读者优先,一旦读者进入&&读者很多,写者基本就很难进入了 int number = *(int *)arg; while (true) { pthread_rwlock_rdlock(&rwlock); // 读者加锁 std::cout << "读者" << number << " 正在读取数据,数据是:" << shared_data << std::endl; sleep(1); // 模拟读取操作 pthread_rwlock_unlock(&rwlock); // 解锁 } delete (int*)arg; return NULL; } // 写者线程函数 void *Writer(void *arg) { int number = *(int *)arg; while (true) { pthread_rwlock_wrlock(&rwlock); // 写者加锁 shared_data = rand() % 100; // 修改共享数据 std::cout << "写者" << number << " 正在写入,新的数据是:" << shared_data << std::endl; sleep(2); // 模拟写入操作 pthread_rwlock_unlock(&rwlock); // 解锁 } delete (int*)arg; return NULL; } int main() { srand(time(nullptr) + getpid()); pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁 // 可以更改读写数量配比,观察现象 const int reader_num = 2; const int writer_num = 2; const int total = reader_num + writer_num; pthread_t threads[total]; // 假设读者和写者数量相等 // 创建读者线程 for (int i = 0; i < reader_num; ++i) { int *id = new int(i); pthread_create(&threads[i], NULL, Reader, id); } // 创建写者线程 for (int i = reader_num; i < total; ++i) { int *id = new int(i - reader_num); pthread_create(&threads[i], NULL, Writer, id); } // 等待所有线程完成 for (int i = 0; i < total; ++i) { pthread_join(threads[i], NULL); } pthread_rwlock_destroy(&rwlock); // 销毁读写锁 return 0; }读者0 正在读取数据,数据是:0
读者1 正在读取数据,数据是:0
写者0 正在写入,新的数据是:42
读者0 正在读取数据,数据是:42
读者1 正在读取数据,数据是:42
写者1 正在写入,新的数据是:17
读者0 正在读取数据,数据是:17
写者0 正在写入,新的数据是:76
读者1 正在读取数据,数据是:76
按理来说是这样,但因为这样的写法,写者压根抢不过读者,如果想看到写入,尝试注释和放开各个sleep


























