【C++】多线程同步三剑客介绍

发布于:2025-07-16 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

条件变量

头文件

主要操作函数

1、等待操作

2、唤醒操作

使用示例

信号量

头文件

主要操作函数

1、信号量初始化

2、等待操作(P操作)

3、信号操作(V操作)

4、获取信号量值 

5、销毁信号量

使用示例

互斥锁

头文件

使用示例


当我们需要给多个线程的指定执行顺序的时候,我们通常有多种方法:

  • 条件变量
  • 信号量
  • 互斥锁

在这篇文章里,会介绍如何使用这三种方式来为多个线程指定执行顺序,以及在使用的时候需要主义的地方。

条件变量

        条件变量是C++11引入的同步原语,用于在多线程环境中实现线程间的等待和通知机制。它允许一个或多个线程等待某个条件成立,当条件满足时,其他线程可以通知等待的线程继续执行,一般需要配合unique_lock使用。

头文件

#include <condition_variable>

主要操作函数

1、等待操作

a)基本形式

void wait(std::unique_lock<std::mutex>& lock);

b)带谓词形式

template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

 两者的区别在于处理虚假唤醒的情况比较明显,这个在后面介绍哈。

2、唤醒操作

a)唤醒单个线程

void notify_one() noexcept;

特点:唤醒等待队列中的一个线程,具体是哪个线程是未定义的

b)唤醒所有线程

void notify_all() noexcept;

特点:唤醒等待队列中的所有线程,性能开销比较大,但是确保所有等待线程都被唤醒。

这里需要介绍一下虚假唤醒的问题

        虚假唤醒是指线程在没有收到notify_one或者notify_all调用的情况,从wait状态中被唤醒。为什么会出现虚假唤醒的情况呢?因为可能会出现系统信号中断条件变量的等待(SIGINT),或者因为底层I/O操作等底层系统调用中断,导致pthread_cond_wait() 被中断返回,因此出现虚假唤醒的情况。

        在上面,我们介绍了两种等待的方式,他们在处理虚假唤醒的情况表现有所不同。

        带谓词的等待方式,会自动处理虚假唤醒,不需要我们再进行手动处理,那么他是怎么做到自动处理的呢,他的内部实现等价如下代码,就是在循环中不断判断条件是否满足,以此来处理虚假唤醒的情况。

// 带谓词的wait()函数的内部实现等价于:
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred) {
    while (!pred()) {        // 关键:自动循环检查
        wait(lock);          // 调用基本的wait()
    }
    // 退出循环时,保证 pred() 返回 true
}

        基本的等待方式 需要我们手动处理虚假唤醒的情况,如下代码是有问题的:

void wrong_basic_wait() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // 错误:只等待一次,不处理虚假唤醒
    cv.wait(lock);
    
    // 假设条件一定满足 - 危险!
    if (data_ready) {
        process_data();
    }
}

        如果因为底层系统调用中断了等待,但是此时条件并不满足,比如数据并未准备好,会出现未定义的情况,因此,我们需要模仿带谓词的等待方式的等价写法,在循环中判断,如下:

void correct_basic_wait() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // 正确:使用循环处理虚假唤醒
    while (!condition_satisfied()) {
        cv.wait(lock);
        // 如果是虚假唤醒,循环会继续等待
        // 如果条件真的满足,循环会退出
    }
    
    // 这里保证条件一定满足
    process_data();
}

使用示例

1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/

class Foo {
    condition_variable m_cv;
    mutex m_mtx;
    int m_nFlg;
public:
    Foo() {
        
        m_nFlg=1;
    }

    void first(function<void()> printFirst) {
        
        // printFirst() outputs "first". Do not change or remove this line.
        unique_lock<mutex> lock(m_mtx);
        m_cv.wait(lock,[=](){

            return m_nFlg==1;
        });
        printFirst();
        m_nFlg=2;
        m_cv.notify_all();
    }

    void second(function<void()> printSecond) {
        
        // printSecond() outputs "second". Do not change or remove this line.
        unique_lock<mutex> lock(m_mtx);
        m_cv.wait(lock,[=](){

            return m_nFlg==2;
        });
        printSecond();
        m_nFlg=3;
        m_cv.notify_all();
    }

    void third(function<void()> printThird) {
        
        // printThird() outputs "third". Do not change or remove this line.
        unique_lock<mutex> lock(m_mtx);
        m_cv.wait(lock,[=](){

            return m_nFlg==3;
        });
        printThird();
        m_nFlg=1;
        m_cv.notify_all();
    }
};

信号量

信号量的本质就是一个非负整数计数器,支持两个原子操作:P(等待/减少)、V(信号/增加)

头文件

#include <semaphore.h>

主要操作函数

1、信号量初始化

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

参数说明:

  • sem:指向信号量(sem_t)的指针
  • pshared:0表示线程间共享,非0表示进程间共享
  • value:信号量的初始值

返回值

  • 返回0:初始化成功
  • 返回-1:初始化失败,同时设置errno的错误码。

2、等待操作(P操作)

信号量等待有三种方式

a)sem_wait()-阻塞等待

int sem_wait(sem_t *sem);

特点:如果信号量值为0,线程会一直阻塞等待,知道信号量可用。

b)sem_trywait()-非阻塞等待

int sem_trywait(sem_t *sem);

特点:非阻塞等待,立即返回,不会等待,如果信号量不可用,立即返回-1,不会造成线程阻塞的情况,适用于轮询场景

c)sem_timedwait() - 超时等待

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

特点:在指定时间内等待,超时后返回-1,使用绝对时间戳,不是相对时间。

这个参数比较多,这里演示下用法:

struct timespec结构体用于存储超时时间:

  • tv_sec:秒数
  • tv_nsec:纳秒数
#include <semaphore.h>
#include <iostream>
#include <time.h>
#include <errno.h>

void timed_work() {
    struct timespec timeout;
    clock_gettime(CLOCK_REALTIME, &timeout);
    timeout.tv_sec += 5;  // 5秒后超时
    
    int result = sem_timedwait(&sem, &timeout);
    
    if (result == 0) {
        std::cout << "在超时前获取到信号量" << std::endl;
        // 执行临界区代码
        sem_post(&sem);
    } else {
        if (errno == ETIMEDOUT) {
            std::cout << "等待超时,放弃获取" << std::endl;
        }
    }
}

 下面的代码作用是获取当前的系统时间,CLOCK_REALTIME表示使用系统实时时钟

clock_gettime(CLOCK_REALTIME, &timeout);

3、信号操作(V操作)

释放信号量,也就是将信号量的值+1。

int sem_post(sem_t *sem);

4、获取信号量值 

int sem_getvalue(sem_t *sem, int *sval);

5、销毁信号量

这种只能用于未命名的信号量,比如我们直接定义的sem_t sem,就属于未命名信号量

int sem_destroy(sem_t *sem);

使用示例

1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/这题希望我们指定三个线程的执行顺序,我们可以定义三个信号量来进行控制

class Foo {
    sem_t s1,s2,s3;
public:
    Foo() {
        
        sem_init(&s1,0,1);
        sem_init(&s2,0,0);
        sem_init(&s3,0,0);
    }
    ~Foo() {
        
        sem_destroy(&s1);
        sem_destroy(&s2);
        sem_destroy(&s3);
    }
    void first(function<void()> printFirst) {
        
        // printFirst() outputs "first". Do not change or remove this line.
        sem_wait(&s1);
        printFirst();
        sem_post(&s2);
    }

    void second(function<void()> printSecond) {
        
        // printSecond() outputs "second". Do not change or remove this line.
        sem_wait(&s2);
        printSecond();
        sem_post(&s3);
    }

    void third(function<void()> printThird) {
        
        // printThird() outputs "third". Do not change or remove this line.
        sem_wait(&s3);
        printThird();
        sem_post(&s1);
    }
};

互斥锁

头文件

#include <mutex>

使用示例

因为互斥锁比较简单这里,直接展示使用示例:1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/

class Foo {
    mutex mtx1,mtx2,mtx3;
public:
    Foo() {
        
        mtx2.lock();
        mtx3.lock();
    }

    void first(function<void()> printFirst) {
        
        // printFirst() outputs "first". Do not change or remove this line.
        mtx1.lock();
        printFirst();
        mtx2.unlock();
    }

    void second(function<void()> printSecond) {
        
        // printSecond() outputs "second". Do not change or remove this line.
        mtx2.lock();
        printSecond();
        mtx3.unlock();
    }

    void third(function<void()> printThird) {
        
        // printThird() outputs "third". Do not change or remove this line.
        mtx3.lock();
        printThird();
        mtx1.unlock();
    }
};


网站公告

今日签到

点亮在社区的每一天
去签到