【Linux】Linux 多线程
目录
- 1. Linux线程概念
- 2. 重谈进程地址空间---页表
- 2.1 如何由虚拟地址转化为物理地址的
- 3. pthread库调用接口
- 3.1 线程的创建---pthread_create
- 3.2 线程等待---pthread_join
- 3.3 线程的退出
- 3.4 分离线程
- 4. 线程库
- 5. 线程ID
- 6. Linux线程互斥
- 6.1 锁
- 6.2 锁的接口
- 6.2.1 互斥量的初始化
- 6.2.2 销毁互斥量
- 6.2.3 互斥量加锁和解锁
- 6.3 锁的原理
- 6.4 条件变量
- 7. POSIX信号量
- 8. 生产者消费者模型
- 8.1 基于阻塞队列(BlockQueue)的生产消费模型(含实现代码)
- 8.2 基于环形队列(RingQueue)的生产消费模型(含实现代码)
- 9. 自己封装线程、std::thread的基本使用
- 9.1 线程的封装
- 9.2 std::thread的基本使用
- 10. 线程池
1. Linux线程概念
线程:是进程内的一个执行分支。线程的执行拉度)要比进程要细。
线程分为主线程和副线程,线程共享绝大部分的进程地址空间内容。
Linux中的线程也被叫做轻量化进程。
如何理解进程和线程:
线程是操作系统调度的基本单位!
进程是承担分配系统资源的基本实体,进程内包括了线程
为什么线程比进程要更轻量化?:
- 创建和释放更加轻量化
- 切换更加轻量化
线程共享进程数据,但也拥有自己的一部分数据:
- 一组寄存器
- 栈
- 信号屏蔽字
各线程共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
2. 重谈进程地址空间—页表
2.1 如何由虚拟地址转化为物理地址的
下面以32位虚拟地址(虚拟地址为4字节)为例:
虚拟地址为4字节,32个比特位,分为:10+10+12
3. pthread库调用接口
内核中有没有很明确的线程的概念呢?没有的。线程就是轻量级进程的概念。并不会给我直接给我们提供轻量级进程的系统调用!
所以在pthread线程库将轻量级进程接口进行了封装----pthread为用户提供直接线程的接口,pthread是在应用层。Linux中编写多线程代码需要使用第三方pthread库!所以编译时必须加:-lpthread
3.1 线程的创建—pthread_create
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
thread:输出形参数,线程id
attr:线程的属性。可默认,输入nullptr
start_routine:线程的执行函数
arg:输入形参数,arg为线程的执行函数中的参数
返回值:
成功为0,失败放回错误码。
3.2 线程等待—pthread_join
int pthread_join(pthread_t thread, void **value_ptr);
- thread:线程ID
- value_ptr:它指向一个指针,后者指向线程执行函数(即start_routine对应的函数)的返回值。
如果start_routine对应的函数的返回值为退出码或退出数据,则pthread_join就可以得到退出码或退出数据。 - 返回值:成功返回0;失败返回错误码
注意事项:
- 主线程创建了副线程,就承担了副线程的管理工作,意味着要在副线程退出之后,调用 pthread_join等待函数,才能退出,如果先推,则会造成内存泄露。
- 主线程的线程id和进程的id相同,副线程则不同。
3.3 线程的退出
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。exit:结束进程
- 线程可以自己调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
void pthread_exit(void *value_ptr);
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
value_ptr 是线程退出时传递的返回值指针,可自己设置int pthread_cancel(pthread_t thread); //thread:线程ID
当线程被别的线程调用pthread_ cancel终止时,返回值为(void*)1
返回值:成功返回0;失败返回错误码
3.4 分离线程
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
- 该函数可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
- joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
int pthread_detach(pthread_t thread);
4. 线程库
- 一个线程控制块对应一个线程,线程控制块里有指针指向回调函数和独立栈。
- 线程库给我们维护线程概念,通过先描述后组织。
- 执行后,线程库会被加载到内存中。
5. 线程ID
-
每一个线程的库级别的tcb的起始地址,叫做线程的tid!
-
除了主线程,所有其他线程的独立栈,都在共享区。具体来讲是在pthread库中,tid指向的用户tcb中!
-
pthread_ create函数会产生一个线程ID,就是图中的tid,存放在第一个参数指向的地址中。
-
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
-
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
6. Linux线程互斥
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
6.1 锁
当多个线程访问全局变量ticket,并进行ticket–; 时,会出现下面的情况:
怎么解决?
对共享数据的任何访问,保证任何时候只有一个执行流访问!——互斥!!——用锁
6.2 锁的接口
6.2.1 互斥量的初始化
互斥量=锁
方法1,静态分配(互斥锁是全局变量或静态变量):
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配
pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *restrict_mutex,
const pthread_mutexattr_t *restrict_attr);
参数:
mutex:要初始化的互斥量
attr:NULL
6.2.2 销毁互斥量
销毁互斥量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
6.2.3 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
6.3 锁的原理
lock:movb $0, %al ; AL = 0(尝试获取锁)xchgb %al, mutex ; 原子交换 AL 和 mutex 的值if (%al > 0) { ; 如果 AL > 0(获取锁成功)return 0; ; 进入临界区} else {挂起等待; ; 锁被占用,自旋等待}goto lock; ; 继续尝试获取锁unlock:movb $1, mutex ; 释放锁(mutex = 1)唤醒等待Mutex的线程; ; 通知其他线程return 0;
关键点
xchgb %al, mutex
- 这是一个 原子交换 操作,保证
mutex
的修改不会被其他线程干扰。 - 如果
mutex
初始为1
(可用),交换后:AL = 1
(成功获取锁)mutex = 0
(锁被占用)
- 如果
mutex
为0
(被占用),交换后:AL = 0
(获取锁失败)mutex
仍然是0
(锁仍被占用)
if (AL > 0)
- 如果
AL == 1
,说明锁获取成功,可以进入临界区。 - 如果
AL == 0
,说明锁被占用,需要 自旋等待(忙等待或让出 CPU)。
unlock
部分
movb $1, mutex
直接释放锁(mutex = 1
)。
交换的本质:
把内存中的数据,交换到CPU的寄存器中,成为某一线程的上下文
6.4 条件变量
条件变量的作用:使满足条件的队列沉睡,等待条件满足后唤醒。使线程对于资源的竞争有序,防止饥饿情况。
条件变量函数 初始化:
int pthread_cond_init(pthread_cond_t *restrict_cond,
const pthread_condattr_t *restrict_attr);
参数:
restrict_cond:要初始化的条件变量
restrict_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:互斥量
唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond); 将队列所在的线程全部唤醒
int pthread_cond_signal(pthread_cond_t *cond); 只唤醒队首线程
7. POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
信号量的本质是一把记数器,且信号量的增减操作都是原子的。
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:信号量指针
pshared:0 表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:申请资源,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
如果信号量的值 > 0,则直接减 1,并继续执行。
如果信号量的值 ≤ 0,则当前线程/进程会被阻塞,直到信号量变为正数(由其他线程调用sem_post() 释放资源)。
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
8. 生产者消费者模型
为何要使用生产者消费者模型:
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者模型优点:
1.解耦
2.支持并发
3.支持忙闲不均
8.1 基于阻塞队列(BlockQueue)的生产消费模型(含实现代码)
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
C++ queue模拟阻塞队列的生产消费模型:
BlockQueue.hpp:
#pragma once#include<iostream>
#include<string>
#include<queue>
#include<pthread.h>
using namespace std;template <class T>
class BlockQueue
{static const int defaultnum=5;
public:BlockQueue(int maxcap=defaultnum):maxcap_(maxcap){maxcap_=maxcap;pthread_mutex_init(&mutex_,NULL);pthread_cond_init(&p_cond_,NULL);pthread_cond_init(&c_cond_,NULL);}void push(const T &in) //生产者{pthread_mutex_lock(&mutex_);while(q_.size()==maxcap_) //满了,不要生产{pthread_cond_wait(&p_cond_,&mutex_);}q_.push(in);//if(q_.size()<low_water_) pthread_cond_signal(&c_cond_);pthread_cond_signal(&c_cond_);pthread_mutex_unlock(&mutex_);}T pop() //消费者{pthread_mutex_lock(&mutex_);while(q_.empty()) //空了,不要去消费,在这排队等{pthread_cond_wait(&c_cond_,&mutex_);}T out=q_.front();q_.pop();//if(q_.size()>high_water_) pthread_cond_signal(&p_cond_);pthread_cond_signal(&p_cond_);pthread_mutex_unlock(&mutex_);return out;}~BlockQueue(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&p_cond_);pthread_cond_destroy(&c_cond_);}
private:queue<T> q_; //仓库队列int maxcap_; //仓库容量/阻塞队列大小pthread_mutex_t mutex_; //整个阻塞队列中只能有一个生产者或消费者,生产者和消费者也不能同时在队列中,因为无法判断生产者消费者何时访问队列中的同一个Tpthread_cond_t p_cond_;pthread_cond_t c_cond_; // int low_water_;// int high_water_;
};
Task.hpp:
#include<string>
#include<iostream>string opers="+-*/%";enum{DivZero=1,ModZero,UnKnown
};using namespace std;
class Task
{
public:Task(int data1,int data2,char oper){data1_=data1;data2_=data2;oper_=oper;}void run(){switch(oper_){case '+':result_=data1_+data2_;break;case '-':result_=data1_-data2_;break;case '*':result_=data1_*data2_;break;case '/':if(data2_==0) exitcode_=DivZero;else result_=data1_/data2_;break;case '%':if(data2_==0) exitcode_=ModZero;else result_=data1_%data2_;break;default:exitcode_=UnKnown;}}void operator()(){run();}string GetTask(){string s;s+=to_string(data1_);s+=oper_;s+=to_string(data2_);s+="=?";return s;}string GetResult(){string s;s+=to_string(data1_);s+=oper_;s+=to_string(data2_);s+="=";s+=to_string(result_);s+=", exitcode_ = ";s+=to_string(exitcode_);return s;}
private:int data1_;int data2_;char oper_;int result_;int exitcode_;
};
main.cc:
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <unistd.h>void *Consumer(void *args)
{BlockQueue<Task> *q = static_cast< BlockQueue<Task> *>(args);while (true){Task t = q->pop();t();//usleep(1);cout<<"处理了一个任务:"<<t.GetResult()<<", thread id :"<<pthread_self()<<endl;//sleep(1);}return nullptr;
}void *Productor(void *args)
{BlockQueue<Task> *q = static_cast< BlockQueue<Task> *>(args);while (true){int data1 = rand() % 10 + 1;int data2 = rand() % 10;char oper = opers[rand() % opers.size()];usleep(1);Task t(data1, data2, oper);q->push(t);cout<<"发送了一个任务 : "<<t.GetTask()<<", thread id :"<<pthread_self()<<endl;sleep(1);}return nullptr;
}int main()
{srand(time(nullptr) ^ getpid());BlockQueue<Task> q;pthread_t c[3], p[5];for (int i = 0; i < 3; i++){pthread_create(c + i, nullptr, Consumer, &q);}for (int i = 0; i < 5; i++){pthread_create(p + i, nullptr, Productor, &q);}for (auto x : c){pthread_join(x,nullptr);}for (auto x : p){pthread_join(x,nullptr);}return 0;
}
8.2 基于环形队列(RingQueue)的生产消费模型(含实现代码)
-
BlockQueue:
-
锁 :互斥
-
条件变量:同步
-
基于阻塞队列的生产消费模型,一段时间内,只允许一个角色在队列中,即要么生产者,要么消费者,这是通过生成者和消费者使用同一个锁实现的。为什么要这样呢?这是为了防止同时访问同一份资源
-
-
RingQueue:
-
POSIX信号量的PV操作:是原子的
-
POSIX信号量的PV操作:同步
-
锁:互斥
-
在基于环形队列(RingQueue)的生产消费模型中,当生产者和消费者访问同一个空间时,要么是队列为空,要么队列为满。
当为空时,消费者的信号量为空,被阻塞,只有生产者可以访问该空间。
当为满时,生产者的信号量为空,被阻塞,只有消费者者可以访问该空间。
所以不用担心同一个空间被生产者和消费者同时访问。所以生产者和消费者可以同时在队列中进行生产和消费。所以生产者和消费者可以分别使用一把锁。
-
- 环形队列采用数组模拟,用模运算来模拟环状特性。
- 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以一般可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态(如下图)。但是在我们这个生产消费模型中,不需要这样做,可以直接让环形结构起始状态和结束状态都是一样的,同时也需要判断空和满,因为会有POSIX信号量来维持同步关系。
代码gittee链接:
基于环形队列(RingQueue)的生产消费模型
RingQueue.hpp:
#include<iostream>
#include<vector>
#include<semaphore.h>
#include"Task.hpp"
#include<pthread.h>
using namespace std;const int DefaultNum=10;template<class T>
class RingQueue
{
private:void P(sem_t& sem) //-{sem_wait(&sem);}void V(sem_t& sem) //+{sem_post(&sem);}public:RingQueue(int cap=DefaultNum){cap_=cap;q.resize(cap_);pthread_mutex_init(&c_mutex,nullptr); pthread_mutex_init(&p_mutex,nullptr);sem_init(&cdata_sem_,0,0); sem_init(&pspace_sem_,0, cap_);}void Push(const T& in) //生产者{P(pspace_sem_); //减,只能先减,不能先加pthread_mutex_lock(&p_mutex);q[p_step_]=in;p_step_=(p_step_+1)%cap_;pthread_mutex_unlock(&p_mutex);V(cdata_sem_); //加}void Pop(T& out) //消费者{P(cdata_sem_);pthread_mutex_lock(&c_mutex); out=q[c_step_];c_step_=(c_step_+1)%cap_;pthread_mutex_unlock(&c_mutex);V(pspace_sem_);}~RingQueue(){pthread_mutex_destroy(&c_mutex);pthread_mutex_destroy(&p_mutex);sem_destroy(&cdata_sem_);sem_destroy(&pspace_sem_);}
private:vector<T> q;int cap_;pthread_mutex_t c_mutex;pthread_mutex_t p_mutex;sem_t cdata_sem_;sem_t pspace_sem_;int c_step_=0;int p_step_=0;
};
Task.hpp:
#pragma once
#include <iostream>
#include <string>std::string opers="+-*/%";enum{DivZero=1,ModZero,Unknown
};class Task
{
public:Task(){}Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0){}void run(){switch (oper_){case '+':result_ = data1_ + data2_;break;case '-':result_ = data1_ - data2_;break;case '*':result_ = data1_ * data2_;break;case '/':{if(data2_ == 0) exitcode_ = DivZero;else result_ = data1_ / data2_;}break;case '%':{if(data2_ == 0) exitcode_ = ModZero;else result_ = data1_ % data2_;} break;default:exitcode_ = Unknown;break;}}void operator ()(){run();}std::string GetResult() //r: a+b=c[code:1]{std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=";r += std::to_string(result_);r += "[code: ";r += std::to_string(exitcode_);r += "]";return r;}std::string GetTask() //r: a+b=?{std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=?";return r;}~Task(){}private:int data1_;int data2_;char oper_;int result_;int exitcode_;
};
main.cc:
#include<iostream>
#include"RingQueue.hpp"
#include"Task.hpp"
#include<pthread.h>
#include<time.h>
#include<unistd.h>
#include<string>
using namespace std;struct ThreadData
{RingQueue<Task> *rq;string ThreadName;
};//string opers="+-*/%";void *Producer(void* args)
{ThreadData* td=static_cast<ThreadData*>(args);RingQueue<Task> *rq=td->rq;string ThreadName=td->ThreadName;while(true){int data1=rand()%100;int data2=rand()%100;char oper=opers[rand()%opers.size()];Task t(data1,data2,oper);rq->Push(t);cout<<ThreadName<<" send a task: "<<t.GetTask()<<endl;sleep(1);}delete td;return nullptr;
}void *Consumer(void* args)
{ThreadData* td=static_cast<ThreadData*>(args);RingQueue<Task> *rq=td->rq;string ThreadName=td->ThreadName;while(true){Task t;rq->Pop(t);t.run();cout<<ThreadName<<" handle a task: "<<t.GetResult()<<endl;}delete td;return nullptr;
}int main()
{srand(time(nullptr)^getpid());RingQueue<Task> *rq=new RingQueue<Task>(50);pthread_t p[5],c[3];for(int i=0;i<5;i++){ThreadData* td=new ThreadData;td->ThreadName="Producer_"+to_string(i);td->rq=rq;pthread_create(p+i,nullptr,Producer,(void*)td);}for(int i=0;i<3;i++){ThreadData* td=new ThreadData;td->ThreadName="Consumer_"+to_string(i);td->rq=rq;pthread_create(c+i,nullptr,Consumer,(void*)td);}for(auto x:p){pthread_join(x,nullptr);}for(auto x:c){pthread_join(x,nullptr);}delete rq;return 0;
}
9. 自己封装线程、std::thread的基本使用
9.1 线程的封装
gittee链接如下:线程的封装
Thread.hpp:
#include<iostream>
#include<string>
#include<unistd.h>
#include <pthread.h>
#include<vector>
using namespace std;static int num=1;
typedef void(*callback_t)(); //void func() 形式的函数的函数指针,callback_t包含在线程的执行函数里,是回调函数//对线程进行封装
class Thread
{
public:static void* Routine(void* args) //args:cb_ 为什么要加static,不加的话,Routin参数里就会有一个 this 指针{Thread* self=static_cast<Thread*>(args);self->cb_();return nullptr;}void Join(){if(isrunnig_){pthread_join(id_,nullptr);isrunnig_=false;}}public:Thread(callback_t cb):ThreadName_(""), id_(0), isrunnig_(false),cb_(cb){}void Run(){//开始创建线程isrunnig_=true;ThreadName_="Thread"+to_string(num++);start_timestamp_=time(nullptr); //time(nullptr): 获取当前系统时间的时间戳pthread_create(&id_,nullptr,Routine,(void*)this); //这里的this指针不能改成cb_,因为函数指针的转换存在风险}~Thread(){Join();}private:string ThreadName_;pthread_t id_;bool isrunnig_; //可表示为线程是否被创建过uint64_t start_timestamp_; //可以计算出线程运行时间callback_t cb_;
};
Main.cc:
#include"Thread.hpp"
using namespace std;void Print()
{cout<<"我是一个封装的线程"<<endl;
}int main()
{vector<Thread> threads;for(int i=0;i<5;i++){threads.push_back(Thread(Print));}for(auto& t:threads){t.Run();}for(auto& t:threads){t.Join();}return 0;}
9.2 std::thread的基本使用
C++ std::thread 简单用法示例:
#include <iostream>
#include <thread>
#include <vector>// 1. 基本线程函数
void hello() {std::cout << "Hello from thread!\n";
}// 2. 带参数的线程函数
void print_numbers(int start, int end) {for (int i = start; i <= end; ++i) {std::cout << i << " ";}std::cout << "\n";
}int main() {// 1. 创建单个线程std::thread t1(hello);t1.join(); // 等待线程结束// 2. 带参数的线程std::thread t2(print_numbers, 1, 5);t2.join();// 3. 使用lambda表达式创建线程std::thread t3([]() {std::cout << "Lambda thread running\n";});t3.join();// 4. 创建多个线程std::vector<std::thread> threads;for (int i = 0; i < 3; ++i) {threads.emplace_back([i]() {std::cout << "Thread " << i << " running\n";});}// 等待所有线程完成for (auto& t : threads) {t.join();}std::cout << "All threads finished\n";return 0;
}
10. 线程池
gittee链接: 线程池
ThreadPool.hpp :
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include<cassert>const int defaultnum=5;class ThreadInfo
{
public:std::string threadname;pthread_t tid;
};template <class T>
class ThreadPool
{
private://有关锁和条件变量的函数的封装void Lock(){pthread_mutex_lock(&mutex_);}void UnLock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_); }void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_); //唤醒的两个条件:有锁,有人唤醒我(有人调用pthread_cond_signal函数)}std::string GetThreadName(pthread_t tid){for(const auto& ti:threads_){if(ti.tid==tid){return ti.threadname;}}return "None";}bool IsTaskEmpty(){return tasks_.empty();}public:static void* HandlerTask(void* args){ThreadPool<T>* tp=static_cast<ThreadPool<T>*>(args);pthread_t tid=pthread_self();std::string threadname=tp->GetThreadName(tid);pthread_detach(tid);//开始执行任务while(true){tp->Lock();while (tp->IsTaskEmpty()){tp->ThreadSleep(); // 默认为阻塞,while防止虚假唤醒 //唤醒的两个条件:有锁,有push任务}T t = tp->PopTask();tp->UnLock();t();std::cout << threadname << " handler a task: " << t.GetResult() << std::endl;}return nullptr;}void PushTask(const T& t){Lock();tasks_.push(t);Wakeup(); //有任务了,唤醒线程执行任务UnLock();}T PopTask() //PopTask不需要加锁,因为在HandlerTask内,PopTask在加锁区域内,并且也不能加锁,加了会导致死锁{assert(pthread_mutex_trylock(&mutex_) != 0);T t=tasks_.front();tasks_.pop();return t;}static ThreadPool<T>* GetInstance() //目的:得到 ThreadPool对象,new一个{if(nullptr==tp_){pthread_mutex_lock(&lock_); //为什么加锁,防止多个线程执行单例模式函数std::cout << "log: singleton create done first!" << std::endl; //单例模式(singleton)的创建已完成tp_=new ThreadPool<T>();pthread_mutex_unlock(&lock_);}return tp_;}void Start(){int nums=threads_.size();for(int i=0;i<nums;i++){//讲线程的信息也放入了threads数组中threads_[i].threadname=std::to_string(i)+"_Thread";pthread_create(&threads_[i].tid,nullptr,HandlerTask,this);}}void Destroy(){if(tp_){delete tp_; //调用析构函数,不可以直接~ThreadPool();tp_ = nullptr;}}private:ThreadPool(int num = defaultnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool() //一般含单例模式的类的析构函数都是private,因为防止外部销毁{pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete; //拷贝构造const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b,拷贝赋值private:std::vector<ThreadInfo> threads_; //放线程std::queue<T> tasks_; //放任务static ThreadPool<T>* tp_; //配合单例模式,且必须要用staticstatic pthread_mutex_t lock_; //只搭配单例模式的锁,使用两把锁,使代码清晰pthread_mutex_t mutex_; pthread_cond_t cond_;
};template <class T>
ThreadPool<T>* ThreadPool<T>::tp_=nullptr; //tp_的初始化,必须加ThreadPool<T>:://为什么必须要设置为static,而不能是非static,并在Start中初始化,因为ThreadPool<Task>::GetInstance()->Start(); GetInstance在Start前面
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER; //lock_的初始化
Main.cc:
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"
#include "Task.hpp"int main()
{std::cout<<"Process running..."<<std::endl;ThreadPool<Task>::GetInstance()->Start(); //线程已准备,等待着任务srand(getpid()^time(nullptr));while(true){//创建任务int a=rand()%10;int b=rand()%10;int len=opers.size();char oper=opers[rand()%len];usleep(10);Task t(a,b,oper);std::cout<<"main thread make task: "<<t.GetTask()<<std::endl;//放入线程池中,线程池会自动处理任务,因为线程已开启,并且一直在循环,不会退出ThreadPool<Task>::GetInstance()->PushTask(t);sleep(1);}ThreadPool<Task>::GetInstance()->Destroy();return 0;
}
Makefile文件和Task.hpp文件:略,可在gittee链接 (上文有) 中查看
相关文章:
【Linux】Linux 多线程
目录 1. Linux线程概念2. 重谈进程地址空间---页表2.1 如何由虚拟地址转化为物理地址的 3. pthread库调用接口3.1 线程的创建---pthread_create3.2 线程等待---pthread_join3.3 线程的退出3.4 分离线程 4. 线程库5. 线程ID6. Linux线程互斥6.1 锁6.2 锁的接口6.2.1 互斥量的初始…...
DAY31
知识点回顾 规范的文件命名规范的文件夹管理机器学习项目的拆分编码格式和类型注解 作业:尝试针对之前的心脏病项目,准备拆分的项目文件,思考下哪些部分可以未来复用。 浙大疏锦行...
大模型应用开发“扫盲”——基于市场某款智能问数产品的技术架构进行解析与学习
本文将从一款问数产品相关技术架构,针对大模型应用开发中的基础知识进行“扫盲”式科普,文章比较适合新手小白,属于是我的学习笔记整理,大佬可以划走啦~产品关键信息已经进行模糊处理,如有侵权请联系删除。 文章目录 前…...
List优雅分组
一、前言 最近小永哥发现,在开发过程中,经常会遇到需要对list进行分组,就是假如有一个RecordTest对象集合,RecordTest对象都有一个type的属性,需要将这个集合按type属性进行分组,转换为一个以type为key&…...
打破建筑与制造数据壁垒:Revit 到 STP 格式转换全攻略(含插件应用 + 迪威模型实战)
引言 在建筑信息模型(BIM)与计算机辅助设计(CAD)领域,数据在不同软件和系统间的高效流转至关重要。Revit 作为 BIM 技术应用的主流软件,常用于建筑设计、施工和运维管理;而 STP(STE…...
RISC-V 开发板 MUSE Pi Pro USB 测试(3.0 U盘,2.0 UVC摄像头)
视频讲解: RISC-V 开发板 MUSE Pi Pro USB 测试(3.0 U盘,2.0 UVC摄像头) 总共开发板有4个USB的A口,1个USB的TypeC口,我们插上两个USB3.0的U盘和一个USB2.0的UVC摄像头来进行测试 lsusb -tv 可以看到有3个US…...
驱动相关基础
一、驱动分类与区别 字符设备驱动 一个字节一个字节进行读写操作的设备,以字符流的形式进行数据传输(如鼠标、键盘、串口)。 块设备驱动 以块为单位进行读写操作的设备,块的大小通常为 512 字节、1024 字节。 块设备驱动主…...
【node.js】核心进阶
个人主页:Guiat 归属专栏:node.js 文章目录 1. Node.js高级异步编程1.1 Promise深入理解1.1.1 创建和使用Promise1.1.2 Promise组合模式 1.2 Async/Await高级模式1.2.1 基本使用1.2.2 并行执行1.2.3 顺序执行与错误处理 1.3 事件循环高级概念1.3.1 事件循…...
高频Java面试题深度拆解:String/StringBuilder/StringBuffer三剑客对决(万字长文预警)
文章目录 一、这道题的隐藏考点你Get到了吗?二、内存模型里的暗战(图解警告)2.1 String的不可变性之谜2.2 可变双雄的内存游戏 三、线程安全背后的修罗场3.1 StringBuffer的同步真相3.2 StringBuilder的裸奔哲学 四、性能对决:用数…...
量子计算的曙光:从理论奇点到 IT 世界的颠覆力量
在信息技术(IT)的飞速发展中,一项前沿技术正以耀眼的光芒照亮未来——量子计算(Quantum Computing)。2025 年,随着量子硬件的突破、算法的优化以及企业对超算能力的渴求,量子计算从科幻梦想逐步…...
c++使用protocol buffers
在 C 里使用 Protocol Buffer,要先定义消息结构,接着生成 C 代码,最后在程序里使用这些生成的代码。 定义消息结构 首先要创建一个.proto文件,在其中定义消息类型和字段。 // person.proto syntax "proto3"; // 指…...
AI驱动发展——高能受邀参加华为2025广东新质生产力创新峰会
当AI浪潮席卷全球产业版图,一场以"智变"驱动"质变"的变革正在发生。5月15日,华为中国行2025广东新质生产力创新峰会璀璨启幕,作为华为生态战略合作伙伴,高能计算机与行业领军者同台论道,共同解码A…...
怎样解决photoshop闪退问题
检查系统资源:在启动 Photoshop 之前,打开任务管理器检查 CPU 和内存的使用情况。如果发现资源占用过高,尝试关闭不必要的程序或重启计算机以释放资源。更新 Photoshop 版本:确保 Photoshop 是最新版本。Adobe 经常发布更新以修复…...
AWS CodePipeline+ Elastic Beanstalk(AWS中国云CI/CD)
问题 最近需要利用AWS云上面的CI/CD部署Spring应用。 一图胜千言 步骤 打开CodePipeline网页,开始管道创建,如下图: 管道设置,如下图: 这里主要设置管道名称,至于服务角色,直接让codepipel…...
人工智能核心知识:AI Agent 的四种关键设计模式
人工智能核心知识:AI Agent 的四种关键设计模式 一、引言 在人工智能领域,AI Agent(人工智能代理)是实现智能行为和决策的核心实体。它能够感知环境、做出决策并采取行动以完成特定任务。为了设计高效、灵活且适应性强的 AI Age…...
Electron+vite+vue3 从0到1搭建项目,开发Win、Mac客户端
随着前端技术的发展,出现了所谓的大前端。 大前端则是指基于前端技术延伸出来的各种终端平台及应用场景,包括APP、桌面端、手表终端、服务端等。 本篇文章主要是和大家一起学习一下使用Electron 如何打包出 Windows 和 Mac 所使用的客户端APPÿ…...
GitLab部署
学git Git最新最新详细教程、安装(从入门到精通!!!!企业级实战!!!工作必备!!!结合IDEA、Github、Gitee实战!!!…...
基于R语言地理加权回归、主成份分析、判别分析等空间异质性数据分析技术
在自然和社会科学领域,存在大量与地理或空间相关的数据,这些数据通常具有显著的空间异质性。传统的统计学方法在处理这类数据时往往力不从心。基于R语言的一系列空间异质性数据分析方法,如地理加权回归(GWR)、地理加权…...
指针深入理解(二)
volatile关键字 防止优化指向内存地址, typedef 指针可以指向C语言所有资源 typedef 就是起一个外号。 指针运算符加减标签操作 指针加的是地址,并且增加的是该指针类型的一个单位,指针变量的步长可以用sizeof(p[0]) 这两个的p1是不一样…...
django回忆录(Python的一些基本概念, pycharm和Anaconda的配置, 以及配合MySQL实现基础功能, 适合初学者了解)
django 说实在的, 如果是有些Python基础或者编程基础, 使用django开发本地网站的速度还是很快的, 特别是配合ai进行使用. 本人使用该框架作业的一个主要原因就是因为要做数据库大作业, 哥们想速通, 结果由于我一开始没有接触过这些方面的知识, 其实也不算快, 而且现在我也没有…...
leetcode hot100刷题日记——5.无重复字符的最长字串
解答:滑动窗口思想(见官方题解) //方法1 class Solution { public:int lengthOfLongestSubstring(string s) {//哈希表记录是否有重复字符unordered_set<char>c;int maxlength0;int ns.size();//右指针初始化为-1,可以假设…...
一文讲清python、anaconda的安装以及pycharm创建工程
软件下载 Pycharm下载地址: https://download-cdn.jetbrains.com.cn/python/pycharm-community-2024.1.1.exe?_gl1*1xfh3l8*_gcl_au*MTg1NjU2NjA0OC4xNzQ3MTg3Mzg1*FPAU*MTg1NjU2NjA0OC4xNzQ3MTg3Mzg1*_ga*MTA2NzE5ODc1NS4xNzI1MzM0Mjc2*_ga_9J976DJZ68*czE3NDczMD…...
[每日一题] 3355. 零数组变换 i
文章目录 1. 题目链接2. 题目描述3. 题目示例4. 解题思路5. 题解代码6. 复杂度分析 1. 题目链接 3355. 零数组变换 I - 力扣(LeetCode) 2. 题目描述 给定一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] [li, ri]。…...
【笔记】与PyCharm官方沟通解决开发环境问题
#工作记录 2025年5月20日 星期二 背景 在此前的笔记中,我们提到了向PyCharm官方反馈了几个关于Conda环境自动激活、远程解释器在社区版中的同步问题以及Shell脚本执行时遇到的问题。这些问题对日常开发流程产生了一定影响,因此决定联系官方支持寻求解…...
mariadb-cenots8安装
更新系统:安装完成 CentOS 8 后,连接到互联网,打开终端并运行以下命令来更新系统,以获取最新的软件包和安全补丁。 bash sudo yum update -y安装 MariaDB:运行以下命令来安装 MariaDB。 bash sudo yum install mariadb…...
Python实现VTK - 自学笔记(4):用Widgets实现三维交互控制
核心知识点 交互器样式(vtkInteractorStyle):自定义鼠标/键盘交互逻辑三维控件(3D Widgets):使用预制控件实现复杂交互回调机制:实现动态数据更新参数化控制:通过控件调整算法参数import vtk# 1. 创建圆锥体数据源 cone = vtk.vtkConeSour…...
在tp6模版中加减法
实际项目中,我们经常需要标签变量加减运算的操作。但是,在ThinkPHP中,并不支持模板变量直接运算的操作。幸运的是,它提供了自定义函数的方法,我们可以利用自定义函数解决:ThinkPHP模板自定义函数语法如下&a…...
Linux:库与链接
库是预先编译好、可执⾏的⼆进制码,可以被操作系统加载到内存中执⾏。 库有两种: 静态库:.a(Linux)、.lib(Windows) 动态库:.so(Linux)、.dil(Windows) 静态库 1.程序在链接时把库的代码链接到可执⾏⽂件中,运⾏时…...
T008-网络管理常用命令:ping,ipconfig,nslookup,route,netstat
ipconfig:网络诊断命令,显示 IP 地址、掩码、网关信息,清除/显示 DNS 缓存信息; route:主要用于管理路由表,确定数据包如何从源主机通过网络到达目的主机 nslookup:用于查询域名到IP地址&…...
Qt文件:XML文件
XML文件 1. XML文件结构1.1 基本结构1.2 XML 格式规则1.3 XML vs HTML 2. XML文件操作2.1 DOM 方式(QDomDocument)读取 XML写入XML 2.2 SAX 方式(QXmlStreamReader/QXmlStreamWriter)读取XML写入XML 2.3 对比分析 3. 使用场景3.1 …...
MySQL 8.0 OCP 英文题库解析(六)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题41~50 试题4…...
微软开放代理网络愿景
🌐 Microsoft的开放式智能代理网络愿景 2025年05月20日 | AI日报  欢迎各位人工智能爱好者 微软刚刚在Build 2025大会上开启了备受期待的AI周活动,通过发布大…...
阿尔泰科技助力电厂——520为爱发电!
当城市的霓虹在暮色中亮起,当千万个家庭在温暖中共享天伦,总有一群默默的 "光明守护者" 在幕后坚守 —— 它们是为城市输送能量的电厂,更是以科技赋能电力行业的阿尔泰科技。值此 520 爱意满满的日子,阿尔泰科技用硬核技…...
微软账户无密码化的取证影响
五月初,微软正式宣布,新创建的微软账户现在将默认为无密码,以实现“更简单、更安全的登录”。这一变化延续了Windows 11所设定的方向,即逐步淘汰传统密码,转而采用更安全、更方便用户的身份验证方法,如PIN码…...
idea部署本地仓库和连接放送远程仓库
1.下载git,安装好后任意地方又键会出现两个带git的东西 2.点击bash here的那个,召唤出git的小黑窗,输入 git config --global user.name "你自己取名" git config --global user.email "你自己输入你的邮箱" 3.打开id…...
4大AI智能体平台,你更适合哪一个呐?
好记忆不如烂笔头,能记下点东西,就记下点,有时间拿出来看看,也会发觉不一样的感受. AI的火热程度,应该说是今年IT行业内最热的话题了,以下是根据我对各个智能体平台的了解和熟悉,按照 平台特点、…...
Pandas:Series和DataFrame的概念、常用属性和方法
本文目录: 一、Series和Dataframe的概念二、创建Series对象三、创建Dataframe对象(一)Series1.Series的常用属性总结如下:2.Series的常用方法总结如下: (二)Dataframe1.Dataframe的常用属性2.Da…...
Index-AniSora论文速读:探索Sora时代动画视频生成的前沿
AniSora: Exploring the Frontiers of Animation Video Generation in the Sora Era 一、引言 论文开篇指出动画产业近年来的显著增长,动画内容的需求不断攀升,但传统动画制作流程存在劳动密集和耗时的问题,如故事板创建、关键帧生成和中间…...
扫盲笔记之NPM
简介 npm,全名 node package manger。 NPM(Node Package Manager)是一个 JavaScript 包管理工具,也是 Node.js 的默认包管理器。 NPM 允许开发者轻松地下载、安装、共享、管理项目的依赖库和工具。网址:https://www…...
【Go-2】基本语法与数据类型
基本语法与数据类型 Go语言作为一种静态类型、编译型语言,拥有简洁且高效的语法结构。本章将深入介绍Go的基本语法和数据类型,帮助你建立扎实的编程基础。 2.1 第一个 Go 程序 编写第一个Go程序是学习任何编程语言的传统步骤。通过一个简单的“Hello,…...
Varlet UI-Material Design风格Vue 3框架移动端组件库
#Varlet UI是什么 在现代Web开发中,Vue 3以其强大的组件系统特性,成为了构建可复用、模块化应用界面的首选框架。而在Vue 3的生态系统中,Varlet UI开源组件库以其高效、一致和可维护的设计,为开发者提供了丰富的工具和资源。本文将…...
Golang的文件上传与下载
## Golang的文件上传与下载 文件上传 在Golang中,我们可以使用 net/http 包来实现文件上传功能。文件上传的一般流程包括创建一个接收上传请求的处理器,解析表单数据,然后获取文件并保存到服务器指定的位置。 创建文件上传接口 首先ÿ…...
信奥赛-刷题笔记-栈篇-T3-P4387验证栈序列0520
总题单 本部分总题单如下 【腾讯文档】副本-CSP-JSNOI 题单 (未完待续) https://docs.qq.com/sheet/DSmJuVXR4RUNVWWhW?tabBB08J2 栈篇题单 P4387 【深基15.习9】验证栈序列 题目描述 给出两个序列 pushed 和 poped 两个序列,其取值从 1 到 n ( n ≤ 10…...
jenkins授权管理.
使用背景: 在企业中可能多个开发组织共用同一个Jenkins服务器, 不会让用户具有管理员权限的, 需要给用户分配对应的Group组织权限。例如: 张三, 属于devops1这个组织, 仅允许张三对devops1组织相关的jenkins作业进行构…...
Ubuntu24.04安装Dify
1、win10上安装docker不顺利 参考:Dify的安装_dify安装-CSDN博客等资料,Dify依赖Docker运行,在Win10上安装Docker,先安装wsl。在PowerShell(管理员)中输入: wsl --install 或显示“找不到指定文件”,或显示…...
Spring Boot 集成 Elasticsearch【实战】
前言: 上一篇我们简单分享了 Elasticsearch 的一些概念性的知识,本篇我们来分享 Elasticsearch 的实际运用,也就是在 Spring Booot 项目中使用 Elasticsearch。 Elasticsearch 系列文章传送门 Elasticsearch 基础篇【ES】 Elasticsearch …...
Spark离线数据处理实例
工具:Jupyter notebook # 一、需求分析 (1)分析美妆商品信息,找出每个“商品小类”中价格最高的前5个商品。 (2)每月订购情况,统计每个月订单的订购数量情况和消费金额。 (3&#x…...
window 安装 wsl + cuda + Docker
WSL 部分参考这里安装: Windows安装WSL2 Ubuntu环境 - 知乎 如果出现错误: WslRegisterDistribution failed with error: 0x800701bc 需要运行:https://crayon-shin-chan.blog.csdn.net/article/details/122994190 wsl --update wsl --shu…...
多通道振弦式数据采集仪MCU安装指南
设备介绍 数据采集仪 MCU集传统数据采集器与5G/4G,LoRa/RS485两种通信功能与一体的智能数据采集仪。该产品提供振弦、RS-485等的物理接口,能自动采集并存储多种自然资源、建筑、桥梁、城市管廊、大坝、隧道、水利、气象传感器的实时数据,利用现场采集的数…...
Linux:进程信号---信号的概念与产生
文章目录 1. 信号的概念1.1 信号1.2 认识信号1.3 signal函数1.4 信号的识别(硬件角度) 2. 信号的产生2.1 键盘组合键2.2 kill命令2.3 系统调用2.4 异常2.5 软件条件 3. core dump 序:在我们的生活中,有很多信号,比如红…...