Linux:线程同步与线程互斥

线程互斥

竞态条件

当多个线程(或进程)并发访问和操作同一个共享资源(如变量、文件、数据库记录等)时,最终的结果依赖于这些线程执行的相对时序(即谁在什么时候执行了哪条指令)。 由于操作系统调度线程执行的顺序具有不确定性,这种依赖时序的行为会导致程序的行为变得不可预测、不一致,甚至完全错误。这种情形叫竞态条件(Race Condition)

为了避免这种情况,需要让线程互斥地访问共享资源。

为此,引入了以下概念:

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。单独的一句汇编语句被认为是原子性的。

互斥量mutex

局部变量在线程的线程栈上,无法被其他线程直接访问,因此不会产生竞态条件,而全局变量被所有线程共享,因此会产生竞态条件,我们以一个抢票程序为例,展示通过互斥量来实现线程互斥:

五个线程对共享资源ticket进行--操作,当ticket==0时停止(模拟抢票)

#include<unistd.h>
#include<pthread.h>
#include<stdio.h>int ticket=50;void* GetTicket(void* arg)
{while(true){if(ticket>0){usleep(800);printf("第%lld号线程抢到到第%d张票\n",(long long)arg,ticket);--ticket;}else{break;}}return 0;
}int main()
{pthread_t t1,t2,t3,t4,t5; pthread_create(&t1,NULL,GetTicket,(void*)1);pthread_create(&t2,NULL,GetTicket,(void*)2);pthread_create(&t3,NULL,GetTicket,(void*)3);pthread_create(&t4,NULL,GetTicket,(void*)4);pthread_create(&t5,NULL,GetTicket,(void*)5);pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);pthread_join(t5,NULL);return 0;
}

可以看到运行结果并不理想:当ticket小于0时还在输出

其原因在于,if(ticket>0)到打印ticket的值这一段并不是一个原子性操作,当线程在ticket>0时进行了if判断,随后可能切换到其他线程执行--ticket操作,等到线程切换回来时就会打印出负数。

概括的讲,ticket全局变量作为线程间的共享资源,线程应该互斥地对其进行修改(或者说线程间对其进行的修改操作应该是原子性的),否则就会因代码执行顺序造成各种问题

为了解决这个问题,我们使用互斥量mutex实现线程互斥:

#include<unistd.h>
#include<pthread.h>
#include<stdio.h>int ticket=50;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;void* GetTicket(void* arg)
{while(true){pthread_mutex_lock(&mutex);if(ticket>0){printf("第%lld号线程抢到到第%d张票\n",(long long)arg,ticket);--ticket;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}usleep(1000);}return 0;
}int main()
{pthread_t t1,t2,t3,t4,t5; pthread_create(&t1,NULL,GetTicket,(void*)1);pthread_create(&t2,NULL,GetTicket,(void*)2);pthread_create(&t3,NULL,GetTicket,(void*)3);pthread_create(&t4,NULL,GetTicket,(void*)4);pthread_create(&t5,NULL,GetTicket,(void*)5);pthread_join(t1,NULL);pthread_join(t2,NULL);pthread_join(t3,NULL);pthread_join(t4,NULL);pthread_join(t5,NULL);return 0;
}

抢票程序运行结果正确:

下面正式解释mutex相关接口和实现原理:

相关接口

Linux下mutex的数据类型为pthread_mutex_t,使用前需要初始化,使用完毕需要销毁

初始化:

两种方式:

定义时初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

调用函数完成初始化:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);

mutex:要初始化的互斥量

attr:设为NULL即可

销毁:

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁

不要销毁⼀个已经加锁的互斥量

已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

调⽤ pthread_ lock 时,可能有以下两种情况:

情况1:互斥量处于未锁状态,该函数会将互斥量锁定,继续执行后面的代码

情况2:其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么线程会陷⼊阻塞状态,等待互斥量解锁后再次试图申请互斥量。

总结使用mutex的方法:

首先定义一个能被多线程共享的mutex变量(全局变量或静态变量),当访问共享资源时加锁,访问结束后解锁

实现原理

了解了mutex的使用方法后,容易产生一个疑惑,对于全局变量这种线程间共享资源,我们需要定义一个mutex对其进行保护,实现原子性的访问,可是mutex本身同样是一个全局变量,多线程同样要对其进行共享访问,这就意味着其加锁解锁操作本身也必须是原子性的(不会被操作系统的调度机制打断),而这又是怎么实现的?

这就需要探究其实现原理:

可以看到,lock函数首先将0赋值给了一个寄存器,该寄存器内容属于线程上下文,不会被其他线程访问到,随后执行xchgb操作,该操作是一个硬件指令,含义为交换后面的两个操作数,也就是交换寄存器中的值(0)和mutex(mutex是一个共享内容),若寄存器中的内容>0则执行完毕,否则将线程挂起等待,到被唤醒时再次回到lock开头。

整个lock函数每次执行只访问了一次mutex,因此是原子性的。

同理unlock中也只访问了一次mutex,因此是原子性的

总的来说,lock和unlock的实现思路就是只访问一次内存中的mutex变量,其余操作(如判断)则通过寄存器进行,而寄存器属于线程上下文,不会受到线程调度的影响,因此整体来看lock和unlock函数是原子性的

而从线程lock函数后到unlock函数之前,这期间该线程不会访问mutex,此时当其他线程执行lock函数时,只能将其赋值为0(此前mutex已经是0),相当于其他线程对mutex是只读不写的,这样总体看来,在一个线程执行lock函数到unlock函数期间,没有任何一个线程对mutex进行修改操作,这样整个操作就是原子性的了

RALL风格的锁

在理解了mutex的原理后,我们在使用锁时可能会觉得操作太繁琐,要手动的初始化,销毁,加锁,解锁。有没有一种方式,使得我们不用手动的初始化和销毁;只需要手动加锁,即可自动解锁呢。

这就不得不提的RAII风格(获取即是初始化)了。其实现思路在于通过管理对象的生命周期来完成相应的操作。

这里给出RAII风格的互斥锁的实现:

LockGuard.h

#pragma once
#include<pthread.h>class Mutex
{
public:Mutex(const Mutex&)=delete;const Mutex& operator=(const Mutex&)=delete;Mutex(){pthread_mutex_init(&_mutex,NULL);}~Mutex(){pthread_mutex_destroy(&_mutex);}void Lock(){pthread_mutex_lock(&_mutex);}void Unlock(){pthread_mutex_unlock(&_mutex);}pthread_mutex_t* GetMutex(){return &_mutex;}
private:pthread_mutex_t _mutex;
};class LockGuard
{
public:LockGuard(Mutex& mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex& _mutex;
};

使用时,先定义mutex,加锁时定义Lockguard对象,该对象析构时解锁

线程同步

我们已经了解了线程如何互斥地运行,但很多时候,线程的运行不仅应该是互斥地,还应符合一定的先后顺序,这就是线程同步。

实现线程同步有两种方式:使用条件变量/信号量,为了更好的展示其作用,我们稍后会引入一个具体场景

条件变量cond

条件变量的数据类型为pthread_cond_t,与mutex一样需要进行初始化和销毁

初始化:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

或者:

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)

等待:

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

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

mutex:等待期间释放该互斥量,恢复后尝试获取该互斥量

调用该函数会释放指定的互斥锁,并使当前线程阻塞,直到其他线程通过 pthread_cond_signal 或pthread_cond_broadcast函数唤醒。当线程唤醒后,pthread_cond_wait函数会再次获取互斥锁。       

唤醒该条件变量上的所有线程/一个线程:

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

条件变量简单封装

#pragma once
#include"LockGuard.h"class Cond
{
public:Cond(){pthread_cond_init(&_cond,NULL);}~Cond(){pthread_cond_destroy(&_cond);}void Wait(Mutex& mutex){pthread_cond_wait(&_cond,mutex.GetMutex());}void Notify(){pthread_cond_signal(&_cond);}void NotifyAll(){pthread_cond_broadcast(&_cond);}
private:pthread_cond_t _cond;
};

信号量

依赖头文件:

#include<semaphore.h>

信号量数据类型为sem_t,同样需要初始化和销毁:

初始化

int sem_init(sem_t *sem, int pshared, unsigned int value);

pshared:0表⽰线程间共享,⾮零表⽰进程间共享

value:信号量初始值

销毁

int sem_destroy(sem_t *sem);

等待信号量,会将信号量的值减1当信号量为0时陷入阻塞状态,直到信号量大于0时恢复并再次尝试对信号量减一

int sem_wait(sem_t *sem); 

发布信号量,将信号量值加1。

int sem_post(sem_t *sem);

生产者与消费者模型

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,缓解了生产者和消费者忙先不均的问题,具体规则如下:

生产者向阻塞队列里放数据,消费者从阻塞队列中取数据

如果缓冲区已经满了,则生产者线程阻塞;

如果缓冲区为空,那么消费者线程阻塞。

思考一下,阻塞队列作为一个临界资源,被生产者线程和消费者线程共享,因此需要互斥访问,但同时消费者和生产者还需要按照一定的顺序来访问:队列为空则只能生产者访问,队列未满则只能消费者访问,这就需要线程同步了。

下面我们分别用条件变量和信号量来实现生产者消费者模型:

条件变量+互斥锁

#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<queue>int count=0;template<class T>
class BlockQueue
{
private:BlockQueue(int cap=defaultnum):_cap(cap){pthread_mutex_init(&mutex,NULL);pthread_cond_init(&cond_c,NULL);pthread_cond_init(&cond_p,NULL);}
public:~BlockQueue(){pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond_c);pthread_cond_destroy(&cond_p);}static BlockQueue& instance(){static BlockQueue bq;return bq;}void Push(const T& in){pthread_mutex_lock(&mutex);while(isfull()){pthread_cond_wait(&cond_p,&mutex);}_q.emplace(in);printf("生产数据%c剩余%ld个\n",in,_q.size());pthread_cond_broadcast(&cond_c);  pthread_mutex_unlock(&mutex); }void Pop(T& out){pthread_mutex_lock(&mutex);while(isempty()){pthread_cond_wait(&cond_c,&mutex);}out=_q.front();_q.pop();printf("消费数据%c剩余%ld个\n",out,_q.size());pthread_cond_broadcast(&cond_p);pthread_mutex_unlock(&mutex); }bool isfull()const {return _q.size()>=_cap;}bool isempty()const {return _q.size()<=0;}
private:static const int defaultnum=10;std::queue<T> _q;int _cap;pthread_mutex_t mutex;pthread_cond_t cond_c;pthread_cond_t cond_p;
};#define Instance() BlockQueue<char>::instance()void* Producer(void* arg)
{while(1){Instance().Push((char)(rand()%26+'a'));sleep(1);}return nullptr;
}void* Consumer(void* arg)
{while(1){char tmp;Instance().Pop(tmp);sleep(2);}return nullptr;
}int main()
{srand(time(NULL));pthread_t p[3],c[5];for(int i=0;i<3;++i){pthread_create(p+i,NULL,Producer,NULL);}for(int i=0;i<5;++i){pthread_create(c+i,NULL,Consumer,NULL);}for(int i=0;i<3;++i){pthread_join(p[i],NULL);}for(int i=0;i<5;++i){pthread_join(c[i],NULL);}return 0;
}

信号量+互斥锁

#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<queue>int count=0;template<class T>
class BlockQueue
{
private:BlockQueue(int cap=defaultnum):_cap(cap){pthread_mutex_init(&mutex,NULL);sem_init(&full,0,0);sem_init(&empty,0,10);}
public:~BlockQueue(){pthread_mutex_destroy(&mutex);sem_destroy(&full);sem_destroy(&empty);}static BlockQueue& instance(){static BlockQueue bq;return bq;}void Push(const T& in){sem_wait(&empty);pthread_mutex_lock(&mutex);_q.emplace(in);printf("生产数据%c剩余%ld个\n",in,_q.size()); pthread_mutex_unlock(&mutex); sem_post(&full);}void Pop(T& out){sem_wait(&full);pthread_mutex_lock(&mutex);out=_q.front();_q.pop();printf("消费数据%c剩余%ld个\n",out,_q.size());pthread_mutex_unlock(&mutex); sem_post(&empty);}bool isfull()const {return _q.size()>=_cap;}bool isempty()const {return _q.size()<=0;}
private:static const int defaultnum=10;std::queue<T> _q;int _cap;pthread_mutex_t mutex;sem_t full;sem_t empty;
};#define Instance() BlockQueue<char>::instance()void* Producer(void* arg)
{while(1){Instance().Push((char)(rand()%26+'a'));sleep(1);}return nullptr;
}void* Consumer(void* arg)
{while(1){char tmp;Instance().Pop(tmp);sleep(2);}return nullptr;
}int main()
{srand(time(NULL));pthread_t p[3],c[5];for(int i=0;i<3;++i){pthread_create(p+i,NULL,Producer,NULL);}for(int i=0;i<5;++i){pthread_create(c+i,NULL,Consumer,NULL);}for(int i=0;i<3;++i){pthread_join(p[i],NULL);}for(int i=0;i<5;++i){pthread_join(c[i],NULL);}return 0;
}

线程池

进行了线程互斥和线程同步的基本实践后,我们接下来设计一个线程池

该线程池支持日志,工作模式为:向任务队列注入任务->唤醒线程池中的线程->执行任务

(其实也是一个生产者消费者模型),下面进行分段设计,并引入一些设计模式

日志

计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录一场信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。

日志内容包括:

时间戳
日志等级
日志内容

文件名行号

进程,线程相关id信息等。

这里,我们规定日志格式为:

[时间戳][日志等级][进程id][文件名][行号]-支持可变参数的消息内容

同时,我们还希望提供两种输出日志的方案:向控制台输出和向指定文件输出

对此我们通过策略模式来实现:

创建一个策略基类,并基于此创建策略派生类,当要调用指定策略的接口时,就用智能指针创建该对象并调用相应接口

Log.h:

#pragma once
#include<unistd.h>
#include<ctime>
#include<string>
#include<memory>
#include<filesystem>
#include<fstream>
#include<iostream>
#include"LockGuard.h"enum class LogLevel
{DEBUG,INFO,WARNING,ERROR,FATAL,
};class LogStrategy
{
public:LogStrategy()=default;virtual ~LogStrategy()=default;virtual void SyncLog(const std::string &message)=0;
};class ConsoleLogStrategy:public LogStrategy
{
public:~ConsoleLogStrategy(){}void SyncLog(const std::string &message){std::cerr<<message<<'\n';}
};class FileLogStrategy:public LogStrategy
{
public:FileLogStrategy(const std::string& dir,const std::string& filename):_dir_path_name(dir),_file_name(filename){try{std::filesystem::create_directory(_dir_path_name);}catch(const std::exception& e){std::cerr << e.what() << '\n';}}~FileLogStrategy(){}void SyncLog(const std::string &message){std::string target=_dir_path_name+'/'+_file_name;std::ofstream out(target.c_str(),std::ios::app);if(!out.is_open())return;out<<message<<'\n';out.close();}
private:std::string _dir_path_name;std::string _file_name;
};class Logger
{
private:class LogMessage{public:LogMessage(LogLevel type,const std::string& file_name,int line,Logger& logger):_current_time(GetCurrentTime()),_type(type),_pid(getpid()),_file_name(file_name),_line(line),_logger(logger){std::stringstream stringbuffer;stringbuffer<<"["<<_current_time<<"]"<<"["<<LogLevelToString(_type)<<"]"<<"["<<_pid<<"]"<<"["<<_file_name<<"]"<<"["<<_line<<"]"<<"-";_loginfo=stringbuffer.str();}~LogMessage(){LockGuard lockguard(_logger._sync_lock);if(_logger._strategy)_logger._strategy->SyncLog(_loginfo);}template<class T>LogMessage& operator<<(const T& info){std::stringstream stringbuffer;stringbuffer<<info;_loginfo+=stringbuffer.str();return *this;}private:std::string _current_time;LogLevel _type;pid_t _pid;std::string _file_name;int _line;std::string _loginfo;Logger& _logger;};
public:Logger()=default;~Logger()=default;void UseConsoleLogStrategy(){if(dynamic_cast<ConsoleLogStrategy*>(_strategy.get())!=nullptr)return;_strategy=std::make_unique<ConsoleLogStrategy>();}void UseFileLogStrategy(const std::string& dir,const std::string& file_name){if(dynamic_cast<FileLogStrategy*>(_strategy.get())!=nullptr)return;_strategy=std::make_unique<FileLogStrategy>(dir,file_name);}LogMessage operator()(LogLevel type,const std::string& file_name,int line){return LogMessage(type,file_name,line,*this);}
private:static std::string LogLevelToString(LogLevel level){switch(level){case LogLevel::DEBUG:return "Debug";case LogLevel::INFO:return "Info";case LogLevel::WARNING:return "Warning";case LogLevel::ERROR:return "Error";case LogLevel::FATAL:return "Fatal";default:return "None";}}static std::string GetCurrentTime(){time_t current_time=time(NULL);struct tm current_tm;localtime_r(&current_time,&current_tm);char timebuffer[64];snprintf(timebuffer,sizeof(timebuffer),"%4d-%02d-%02d-%02d-%02d-%02d",current_tm.tm_year+1900,current_tm.tm_mon+1,current_tm.tm_mday,current_tm.tm_hour,current_tm.tm_min,current_tm.tm_sec);return timebuffer;}
private:std::unique_ptr<LogStrategy> _strategy=std::make_unique<ConsoleLogStrategy>();Mutex _sync_lock;
};static Logger logger;#define LOG(type) logger(type,__FILE__,__LINE__)
inline void ENABLE_CONSOLE_LOG_STRATEGY()
{logger.UseConsoleLogStrategy();
} 
inline void ENABLE_FILE_LOG_STRATEGY(const std::string& dir=std::filesystem::current_path(),const std::string& file_name="log.txt") 
{logger.UseFileLogStrategy(dir,file_name);
}

执行测试程序,得到结果:

#include"Log.h"int main()
{LOG(LogLevel::INFO)<<"hello,world"<<'-'<<123456<<'-'<<3.14<<'\n';return 0;
}

单例模式

下面我们正式设计线程池,为了方便线程池接口的调用,我们提供了一个接口,返回一个静态线程池对象的引用:

static ThreadPool& instance(){static ThreadPool instance;return instance;}

同时,将线程池的构造函数设为private,这样就不能外部构造线程池对象了,确保一个进程最多只有一个线程池对象,当然单例模式还有其他实现方式,比如这种:

class ThreadPool
{
private:static ThreadPool* p;
public:static ThreadPool* GetInstance() {if(p==nullptr)p=new ThreadPool();return p;}
};

但这种实现存在严重的线程安全问题,简单的来说就是,线程池指针本身也是一个共享资源,如果不加锁保护,可能new多个threadpool对象,造成内存泄漏,当然可以直接加锁解决,但由于调用任何线程池的接口都要先调用该接口获取线程池对象,因此该接口应设计的尽可能高效,由此得到以下版本:

只需在第一次判断指针为空时加锁new线程池对象,其余时刻只需一次判断,无需加锁。

class ThreadPool
{
private:static ThreadPool* inst;static std::mutex lock;
public:static ThreadPool* GetInstance() {if (inst == NULL) {lock.lock();if (inst == NULL) {inst = new ThreadPool();}lock.unlock();}return inst;}
};

看起来十分美好,但实际上由于编译器优化原因,很多时候编译结果并不是所见即所得,对于inst这种频繁访问的指针,在编译器优化时可能会将其放在寄存器中,每次直接从寄存器中读取,而不是内存。

而由于寄存器属于线程上下文,一个线程new了一个对象,修改了寄存器,并不会影响其他线程,这样其他线程也会new对象,造成内存泄漏。

为了解决此问题,我们又不得不用volatile关键字来禁止编译器对该变量进行优化,然而我们即使避开了寄存器,还有高速缓存造成的缓存一致性问题,这个问题与上述问题类似,同样是不太好解决的,事实上,这种双重检定的模式(DCLP)无论如何都存在安全风险,已经被淘汰使用。对于实践而言,用第一种实现是最好的。

​
class ThreadPool
{
private:volatile static ThreadPool* inst;static std::mutex lock;
public:static ThreadPool* GetInstance() {if (inst == NULL) {lock.lock();if (inst == NULL) {inst = new ThreadPool();}lock.unlock();}return inst;}
};​

最后我们给出线程池的全部代码:

Thread.h

#pragma once
#include<unistd.h>
#include<pthread.h>
#include<functional>
#include"LockGuard.h"using thread_func=std::function<void()>;std::uint32_t thread_name_count=0;
Mutex _thread_name_count_lock;class Thread
{
private:enum class ThreadStatus{THREAD_NEW,THREAD_RUNNING,THREAD_STOP};public:Thread(thread_func func):_func(func){SetName();}~Thread()=default;void SetISDetached(bool flag){is_detached=_status==ThreadStatus::THREAD_NEW&&flag;}bool Start(){int ret=pthread_create(&_id,NULL,run,this);return ret==0;}bool Join(){int ret=pthread_join(_id,NULL);return !is_detached&&ret==0;}
private:static void* run(void *obj){auto self=static_cast<Thread*>(obj);self->_status=ThreadStatus::THREAD_RUNNING;pthread_setname_np(pthread_self(),self->_name.c_str());if(self->is_detached)pthread_detach(pthread_self());if(self->_func)self->_func();return nullptr;}void SetName(){LockGuard lockguard(_thread_name_count_lock);_name="Thread-"+std::to_string(++thread_name_count);}
private:std::string _name;pthread_t _id;ThreadStatus _status=ThreadStatus::THREAD_NEW;thread_func _func=nullptr;bool is_detached=false;
};

Thread_Pool.h

#pragma once
#include<vector>
#include<queue>
#include"Log.h"
#include"Thread.h"
#include"Cond.h"using Task=std::function<void()>;class ThreadPool
{
public:static ThreadPool& instance(){static ThreadPool instance;return instance;}void Start(){is_running=true;for(auto& thread:_threads){thread.Start();LOG(LogLevel::INFO)<<"thread start";}}void Stop(){LockGuard lockguard(_mutex);is_running=false;_cond.NotifyAll();}void Wait(){for(auto& thread:_threads){thread.Join();LOG(LogLevel::INFO)<<"thread quit";}}bool Enqueue(const Task &task){bool ret=false;LockGuard lockguard(_mutex);if(is_running){_task_queue.emplace(task);if(_wait_num>0)_cond.Notify();ret=true;LOG(LogLevel::INFO)<<"添加任务";}return ret;}
private:ThreadPool(int thread_num=5):_thread_num(thread_num){for(int i=0;i<_thread_num;++i){_threads.emplace_back([this](){//线程池不运行且无任务->直接结束//线程池不运行且有任务->完成剩余任务后结束//线程池运行且无任务->休眠//线程池运行且有任务->做任务while(true){_mutex.Lock();while(is_running&&_task_queue.empty()){++_wait_num;_cond.Wait(_mutex);--_wait_num;}if(!is_running&&_task_queue.empty()){_mutex.Unlock();break;}Task task=_task_queue.front();_task_queue.pop();_mutex.Unlock();task();}});}}ThreadPool(const ThreadPool&)=delete;ThreadPool operator=(const ThreadPool&)=delete;~ThreadPool()=default;
private:int _wait_num=0;bool is_running=false;int _thread_num;std::vector<Thread> _threads;std::queue<Task> _task_queue;Mutex _mutex;//让线程有序访问任务队列Cond _cond;//任务条件变量
};#define  Instance() ThreadPool::instance()

测试程序:

打印1到10

#include"Log.h"
#include"ThreadPool.h"Mutex mutex;
int main()
{Instance().Start();for(int i=0;i<10;++i){Instance().Enqueue([](){LockGuard lockguard(mutex);static int count=1;std::cout<<count<<'\n';++count;});}Instance().Stop();Instance().Wait();return 0;
}

执行结果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/90907.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/90907.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML 常用标签速查表

HTML 常用标签速查表 &#x1f9f1; 结构类标签 标签含义用途说明<html>HTML文档根元素所有HTML内容的根节点<head>头部信息放置元信息&#xff0c;如标题、引入CSS/JS等<body>页面内容主体所有可视内容的容器&#x1f4dd; 文本与标题标签 标签含义用途说…

1.gradle安装(mac)

1.下载二进制包 官网下载&#xff1a;Gradle Releases 国内镜像&#xff08;腾讯云&#xff09;&#xff1a;https://mirrors.cloud.tencent.com/gradle/ 2.解压并配置环境变量 解压到指定目录&#xff08;示例&#xff1a;/opt/gradle&#xff09; sudo mkdir -p /opt/gr…

Rust赋能土木工程数字化

基于Rust语言在数字化领域应用 基于Rust语言在土木工程数字 以下是基于Rust语言在土木工程数字化领域的30个实用案例,涵盖结构分析、BIM、GIS、传感器数据处理等方向。案例均采用Rust高性能、安全并发的特性实现,部分结合开源库或算法。 结构分析与计算 有限元分析框架 使…

KTH5791——3D 霍尔位置传感器--鼠标滚轮专用芯片

1 产品概述 KTH5791是一款基于3D霍尔磁感应原理的鼠标滚轮专用芯片&#xff0c;主要面向鼠标滚轮的旋转的应用场景。两个 专用的正交输出使该产品可直接替代机械和光学旋转编码器的输出方式&#xff0c;使得鼠标磁滚轮的应用开发工作极简 化即兼容目前所有鼠标的滚轮输出方式。…

决策树(Decision Tree)完整解析:原理 + 数学推导 + 剪枝 + 实战

1️⃣ 什么是决策树&#xff1f;决策树&#xff08;Decision Tree&#xff09;是一种常见的监督学习方法&#xff0c;可用于分类和回归。 其基本思想是&#xff1a;通过特征条件的逐层划分&#xff0c;将数据集分割成越来越“纯净”的子集&#xff0c;直到子集中的样本几乎属于…

C语言:20250728学习(指针)

回顾/*************************************************************************> File Name: demo01.c> Author: 阮> Description: > Created Time: 2025年07月28日 星期一 09时07分52秒**********************************************************…

esp32s3文心一言/豆包(即火山引擎)大模型实现智能语音对话--流式语音识别

一、引言 在之前的帖子《Esp32S3通过文心一言大模型实现智能语音对话》中&#xff0c;我们介绍了如何使用Esp32S3微控制器与文心一言大模型实现基本的智能语音对话功能&#xff0c;但受限于语音识别技术&#xff0c;只能处理2-3秒的音频数据。为了提升用户体验&#xff0c;满足…

面试150 最长递增子序列

思路 定义 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度&#xff0c;初始时每个位置的最长子序列长度为 1。然后通过双重循环遍历每一对元素 j < i&#xff0c;如果 nums[i] > nums[j]&#xff0c;说明 nums[i] 可以接在 nums[j] 的递增序列之后&#xff0c;更新 …

TCP 套接字--服务器相关

1.创建 TCP 套接字int server_sockfd socket(AF_INET,SOCK_STREAM, 0);函数原型&#xff1a;#include <sys/socket.h>int socket(int domain, int type, int protocol);domain协议族&#xff08;地址族&#xff09;AF_INET&#xff08;IPv4&#xff09;type套接字类型SO…

六、搭建springCloudAlibaba2021.1版本分布式微服务-admin监控中心

前言Spring Boot Actuator 是 spring-boot 自带监控功能 &#xff0c;可以帮助实现对程序内部运行情况监控&#xff0c;比如监控状况、Bean 加载情况、环境变量、日志信息、线程信息等。 Spring Boot Admin是一个针对 spring-boot 的 actuator 接口进行 UI 美化封装的监控工具。…

轻量级远程开发利器:Code Server与cpolar协同实现安全云端编码

前言&#xff1a;作为一款专为Web环境设计的VS Code托管方案&#xff0c;Code Server通过精简架构重新定义了远程开发体验。其核心优势在于将完整的编辑器功能封装于轻量容器中——仅需不到200MB内存即可运行基础服务&#xff0c;并支持在树莓派等低性能设备上流畅操作。系统采…

图论:最小生成树

今天要介绍两中最小生成树的算法&#xff0c;分别是prim算法和kruskal算法。 最小生成树是所有节点的最小连通子图&#xff0c;即&#xff1a;以最小的成本&#xff08;边的权值&#xff09;将图中所有节点链接到一起。 图中有n个节点&#xff0c;那么一定可以用n-1条边将所有节…

haproxy七层代理

1、负载均衡Load Balance(LB) 概念 负载均衡&#xff1a;是一种服务或基于硬件设备等实现的高可用反向代理技术&#xff0c;负载均衡将特定的业务(web服务、网络流量等)分担给指定的一个或多个后端特定的服务器或设备&#xff0c;从而提高了 公司业务的并发处理能力、保证了业务…

【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 微博文章数据可视化分析-点赞区间实现

大家好&#xff0c;我是java1234_小锋老师&#xff0c;最近写了一套【NLP舆情分析】基于python微博舆情分析可视化系统(flaskpandasecharts)视频教程&#xff0c;持续更新中&#xff0c;计划月底更新完&#xff0c;感谢支持。今天讲解微博文章数据可视化分析-点赞区间实现 视频…

Redis实战(3)-- 高级数据结构zset

有序集合&#xff08;ZSET&#xff09;&#xff1a;可以用作相关有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据…

Mistral AI开源 Magistral-Small-2507

宣布Magistral——Mistral AI推出的首款推理模型&#xff0c;专精于垂直领域、具备透明化特性与多语言推理能力。 最优秀的人类思维并非线性——它穿梭于逻辑、洞见、不确定性与发现之间。推理型语言模型让我们得以将复杂思考和深度理解交由AI增强或代劳&#xff0c;提升了人类…

【Kotlin】如何实现静态方法?(单例类、伴生对象、@JvmStatic)

静态方法 静态方法&#xff08;类方法&#xff09;&#xff1a;不需要创建实例就可以调用&#xff08;直接通过类名调用&#xff09;的方法 Java 中的静态方法&#xff08;static&#xff09; public class Util {public static void doAction() {//...} }调用&#xff1a;Util…

SQL Schema 和Pandas Schema什么意思

在数据处理和分析领域&#xff0c;SQL Schema 和 Pandas Schema 分别指的是在不同数据处理环境中数据的结构定义&#xff0c;以下为你详细介绍&#xff1a;SQL Schema含义SQL Schema&#xff08;模式&#xff09;是数据库对象的一个逻辑容器&#xff0c;它定义了数据库中表、视…

机器学习(一)KNN,K近邻算法(K-Nearest Neighbors)

&#x1f4a1; 建议初学者掌握KNN作为理解其他复杂算法&#xff08;如SVM、决策树、神经网络&#xff09;的基石。K近邻算法&#xff08;K-Nearest Neighbors, KNN&#xff09;详解&#xff1a;原理、实践与优化K近邻算法&#xff08;K-Nearest NeighboKrs&#xff0c;简称KNN&…

Qt 多线程数据库操作优化

在多线程应用中&#xff0c;数据库操作往往是性能瓶颈与稳定性风险的高发区。当多个线程同时读写数据库时&#xff0c;若处理不当&#xff0c;轻则出现查询卡顿、事务冲突&#xff0c;重则导致数据错乱、连接泄漏甚至程序崩溃。Qt作为跨平台框架&#xff0c;提供了QSql系列类支…