C++多线程
其中,简单的多线程场景,主要使用
thread
,mutex
,condition_variable
这三个
线程(std::thread)
- 概念
简单而言,一个程序中可以跑多个线程,它们间共享该程序的内存空间、代码、数据,如文件描述符句柄。其中main
函数的执行流为主线程。
void func(int n);
std::thread td1(func, 8);
创建线程对象td1
, 并传入该线程所执行的函数func
,后面接该函数所需参数列表。
传递的参数,如果是引用类型,实际会进行值传递,除非使用std::ref()
thread
类不能进行拷贝构造或拷贝赋值,可以移动拷贝构造。
常用方法:
//由父线程调用,阻塞式等待td1线程退出
td1.join();
//返回td1线程是否执行完毕
td1.joinable();
//将td1线程与对其负责的父线程分离,即不用再join
td1.detach();
//返回线程id
td1.get_id();
其中对于子线程的
join
操作是必要的,由父线程join,执行结束后,会释放子线程资源(如该线程的控制块等),避免内存泄漏。而当
detach
后,资源释放的操作交由系统进行
互斥锁(std::mutex)
- 介绍
对于某些资源,不能同时访问/操作,称为临界资源。使用锁,来控制线程间的同步与互斥行为。
使用:
std::mutex mtx;
mtx.lock();
//执行临界区代码
mtx.unlock();
其方法中 lock() 与 try_lock() 的区别:
使用
lock()
,如果是其他线程拥有锁,则当前线程会阻塞;而
try_lock()
,不阻塞,返回false。
使用RAII思想的加锁、解锁: lock_guard 和unique_lock 两者都为类模板。在构造函数中完成加锁操作,在析构函数中完成解锁
std::mutex mtx;
void myfunc()
{
...
{
std::lock_guard<std::mutex> lck(mtx);
//执行临界区代码
}
}
通常使用{}
来限制lock_guard或unique_lock对象的生命周期(加锁的范围)
unique_lock
相比于lock_guard
更为灵活,可以在其生命周期中在进行解锁、加锁等操作std::mutex mtx; void func1() { std::unique_lock<std::mutex> lck(mtx); //... lck.unlock(); func2();//在中途执行非临界区代码 lck.lock(); //... }
在执行func2()时,释放锁资源,使其他线程可以得到该锁来执行操作
其他锁
//增加了超时功能
try_lock_for(duration); //如果在超时时间内成功获取锁,则返回 true,否则返回 false
//递归互斥锁,允许同一线程多次加锁
//读写锁, 同时读,独占写
std::shared_mutex smtx;
//允许多个读
std::shared_lock<std::shared_mutex> lock(smtx);
//独占写
std::unique_lock<std::shared_mutex> lock(smtx);
条件变量(std::condition_variable)
- 介绍
条件变量是一个对象,它能够阻塞调用的线程,直到通知恢复为止
阻塞等待方法:
唤醒等待方法:
使用:
std::mutex mtx;
std::condition_variable cv;
void func()
{
std::unique_lock<std::mutex> lck(mtx);
while(检测条件是否满足)
{
cv.wait(lck);//阻塞线程
}
}
void func()
{
std::unique_lock<std::mutex> lck(mtx);
if(检测条件)
{
cv.notify_one();//唤醒一个阻塞线程
}
}
在wait函数中需要传入
unique_lock
对象,在其内部会进行释放锁的操作,让其他线程可以执行notify操作,在wait内部,如果从阻塞被唤醒,会再次加锁然后返回。
wait()
函数有两个重载,一个是上述代码的形式。
另一个,第二个参数,传入一个函数对象,当其他线程调用notify唤醒时,会执行该函数,如果返回true,则唤醒该线程,否则继续阻塞。
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);
相对于第一个wait函数,使用
while()
循环检查条件是否满足,和传入一个函数对象二次检查某条件意义相似
生产者消费者样例
实现一个阻塞式任务队列
const int g_default_cap = 10;
class BlockQueue
{
typedef shared_ptr<Task> T;
private:
BlockQueue()
{
m_cap = g_default_cap;
}
public:
~BlockQueue()
{}
// 单例
static BlockQueue& Instance()
{
static BlockQueue m_instance;
return m_instance;
}
public:
//生产接口
void push(const T task)
{
std::unique_lock<std::mutex> ulk(m_mutex);
while (isFull())
{
m_pro_cond.wait(ulk); //阻塞
}
pushTask(task);
m_con_cond.notify_one(); //唤醒
}
//消费接口
T pop()
{
std::unique_lock<std::mutex> ulk(m_mutex);
while (isEmpty())
{
m_con_cond.wait(ulk); //阻塞
}
// 条件满足,可以消费
T tmp = popTask();
m_pro_cond.notify_one(); //唤醒
return tmp;
}
private:
bool isEmpty()
{
return m_bq.empty();
}
bool isFull()
{
return m_bq.size() == m_cap;
}
void pushTask(const T in)
{
m_bq.push(in); //生产完成
}
T popTask()
{
T tmp = m_bq.front();
m_bq.pop();
return tmp;
}
private:
int m_cap; //容量
queue<T> m_bq; // blockqueue
mutex m_mutex; //保护阻塞队列的互斥锁
condition_variable m_con_cond; // 让消费者等待的条件变量
condition_variable m_pro_cond; // 让生产者等待的条件变量
};
其中
Task
是对需要执行操作的封装
原子操作
支持原子读写和修改,不需要进行加锁
std::atomic<T> ;//模板类
- 使用
#include <atomic>
std::atomic<int> counter(0); // 原子计数器
//返回原子变量的当前值
int a = counter.load();
//将 100 值存储到原子变量中
counter.store(100);
//操作符重载
counter++;
counter = 10;
信号量
- 介绍
一个计数器
std::counting_semaphore<MaxCount> sem(initial_count);
//MaxCount:信号量计数器的最大值
//initial_count:信号量的初始计数器值
- 使用
#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
// 最多允许 3 个线程同时访问
std::counting_semaphore<3> sem(3);
void task(int id) {
sem.acquire(); // 请求资源
std::cout << "Thread " << id << " is running.\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread " << id << " is done.\n";
sem.release(); // 释放资源
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(task, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
二元信号量,信号量的值只能是 0 或 1。它的行为类似于互斥锁
🦀🦀观看~~