C++多线程

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_guardunique_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。它的行为类似于互斥锁


    🦀🦀观看~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值