C++[第三十三章]--线程同步

在C++中,线程同步的常用方法包括以下几种:

1、互斥锁(mutex):

互斥锁是一种用于保护共享资源的同步原语。使用互斥锁可以确保在任何时候只有一个线程能够访问共享资源,从而避免竞态条件(race condition)的发生。

例子:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        // std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
        // std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
        std::lock_guard<std::mutex> lk(mtx);
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

# if 0
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        mtx.lock();
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        mtx.unlock();
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}
#endif

2、条件变量(condition variable):

条件变量是一种用于在线程之间传递信号的同步原语。条件变量通常与互斥锁一起使用,以等待共享资源的可用性或通知其他线程共享资源已被释放。

它的主要成员函数:

wait:会阻塞当前线程直至条件变量被通知,或虚假唤醒发生。调用wait时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。然后阻塞当前执行线程.

notify:notify_one唤醒某个等待(wait)线程。如果当前没有等待线程,则该函数什么也不做,如果同时存在多个等待线程,则唤醒的线程是不确定的(unspecified)。notify_all则是唤醒所有在等待的线程

wait_for:wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回,与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞.

wait_until: 导致当前线程阻塞直至通知条件变量、抵达指定时间或虚假唤醒发生,可选的循环直至满足某谓词。与 std::condition_variable::wait_for 类似,但是wait_until可以指定一个时间点,在当前线程收到通知或者指定的时间点 abs_time超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_until返回,剩下的处理步骤和 wait_until() 类似。

cv_status:枚举类型,指示函数是否由于超时而返回。该类型是condition_variable和condition_variable_any对象中函数wait_for和wait_until的返回类型。

虚假唤醒:在正常情况下,wait类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。

condition_variable_any:
与 std::condition_variable类似,只不过std::condition_variable_any的 wait 函数可以接受任何 lockable参数,而 std::condition_variable只能接受 std::unique_lock类型的参数,除此以外,和std::condition_variable几乎完全一样。

例子:



#if 1
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
#include <queue>
#include <condition_variable>

//经典的生产者-消费者场景来阐述对于condition_variable的使用,生产者-消费者问题,也称有限缓冲问题,
//是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”,
//在实际运行时会发生的问题。
//生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。
//该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
//要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,
//生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

std::mutex g_cvMutex;
std::condition_variable g_cv;

//缓存区
std::deque<int> g_data_deque;
//缓存区最大数目
const int  MAX_NUM = 30;
//数据
int g_next_index = 0;

//生产者,消费者线程个数
const int PRODUCER_THREAD_NUM  = 3;
const int CONSUMER_THREAD_NUM = 3;

void  producer_thread(int thread_id) {
     while (true) {
         //加锁
         std::unique_lock <std::mutex> lk(g_cvMutex);
         //当队列未满时,继续添加数据
         g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
         g_next_index++;
         g_data_deque.push_back(g_next_index);
         std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
         std::cout << " queue size: " << g_data_deque.size() << std::endl;
         //唤醒其他线程 
         g_cv.notify_all();
     }  //自动释放锁
}

void  consumer_thread(int thread_id) {
    while (true) {
        //加锁
        std::unique_lock <std::mutex> lk(g_cvMutex);
        //检测条件是否达成
        g_cv.wait( lk,   []{ return !g_data_deque.empty(); });
        //互斥操作,消息数据
        int data = g_data_deque.front();
        g_data_deque.pop_front();
        std::cout << "\tconsumer_thread: " << thread_id << " consumer data: ";
        std::cout << data << " deque size: " << g_data_deque.size() << std::endl;
        //唤醒其他线程
        g_cv.notify_all();
    }  //自动释放锁
}

int main() {
    std::thread arrRroducerThread[PRODUCER_THREAD_NUM];
    std::thread arrConsumerThread[CONSUMER_THREAD_NUM];

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
        arrRroducerThread[i] = std::thread(producer_thread, i);
    }

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++) {
        arrConsumerThread[i] = std::thread(consumer_thread, i);
    }

    for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
        arrRroducerThread[i].join();
    }

    for (int i = 0; i < CONSUMER_THREAD_NUM; i++) {
        arrConsumerThread[i].join();
    }
    
    return 0;
}

#endif

#if 0
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

const int buffer_size = 10;
int buffer[buffer_size];
int count = 0;

std::mutex mtx;
std::condition_variable cv;

void producer() {
    for (int i = 0; i < 100; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return count < buffer_size; });
        buffer[count++] = i;
        std::cout << "Produced " << i << std::endl;
        cv.notify_all();
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return count > 0; });
        int data = buffer[--count];
        std::cout << "Consumed " << data << std::endl;
        cv.notify_all();
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

#endif

#if 0
//有一个生产者线程和一个消费者线程。生产者线程会在等待一段时间后设置一个数据准备好的标志,并通知消费者线程可以取出数据。消费者线程会在等待条件变量时使用了 while 循环。
//然而,由于操作系统的调度原因,当消费者线程被唤醒时,即使条件仍未满足,它也会跳过 while 循环并继续执行。因此,在这个例子中,如果消费者线程被虚假唤醒,它会输出 "Consumer woken up!",
//即使实际上并没有数据可用。为了避免虚假唤醒,消费者线程应该使用 while 循环来等待条件变量,并且在循环体中重新检查条件是否满足。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::mutex mutex;
std::condition_variable cv;
bool ready = false;

void consumer() {
    std::cout << "Consumer waiting for data..." << std::endl;
    std::unique_lock<std::mutex> lock(mutex);
    while (!ready) {
        cv.wait(lock);
        std::cout << "Consumer woken up!" << std::endl;
    }
    std::cout << "Consumer got data." << std::endl;
}

void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟生产数据的耗时操作
    {
        std::lock_guard<std::mutex> lock(mutex);
        ready = true;
    }
    std::cout << "Producer produced data." << std::endl;
    cv.notify_all();
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

#endif

3、信号量(semaphore):

信号量是一种计数器,用于在多个线程之间控制并发访问共享资源。当一个线程想要访问共享资源时,它必须先获取信号量,如果信号量的值为0,则线程将被阻塞,直到另一个线程释放信号量。
锁的功能可以使用信号量来实现。事实上,锁可以被看作是一种特殊的信号量,即二元信号量。

#include <iostream>
#include <pthread.h>
#include <semaphore.h>

// 使用信号量保护了共享资源,保证了多个线程对共享资源的访问互斥。
// 其中,sem_wait()函数等待信号量,如果信号量的值为0,则会阻塞等待直到信号量的值大于0;
// sem_post()函数释放信号量,将信号量的值加1。


using namespace std;

sem_t sem;

void *worker1(void *arg)
{
    sem_wait(&sem); //等待信号量
    cout << "worker1 get the semaphore" << endl;
    //处理共享资源
    sem_post(&sem); //释放信号量
    pthread_exit(NULL);
}

void *worker2(void *arg)
{
    sem_wait(&sem); //等待信号量
    cout << "worker2 get the semaphore" << endl;
    //处理共享资源
    sem_post(&sem); //释放信号量
    pthread_exit(NULL);
}

int main()
{
    pthread_t t1, t2;
    sem_init(&sem, 0, 1); //初始化信号量
    pthread_create(&t1, NULL, worker1, NULL);
    pthread_create(&t2, NULL, worker2, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    sem_destroy(&sem); //销毁信号量
    return 0;
}

4、屏障(barrier):

屏障是一种同步原语,用于在多个线程之间协调执行顺序。当多个线程都到达屏障时,它们将被阻塞,直到所有线程都到达屏障后才能继续执行。

例子:

#include <iostream>
#include <thread>
#include <barrier>  //C++20

// 每个线程都模拟了一些工作,然后等待屏障。当所有线程都到达屏障时,它们都会继续执行,
// 并输出"Worker x continued"的信息。最后,主线程输出"All workers finished"的信息,表示所有工作都已经完成。


using namespace std;

const int THREAD_COUNT = 4;

void worker(int id, std::barrier<>& b)
{
    cout << "Worker " << id << " started" << endl;
    //模拟一些工作
    this_thread::sleep_for(chrono::seconds(1));
    cout << "Worker " << id << " finished work" << endl;
    b.wait(); //等待屏障
    cout << "Worker " << id << " continued" << endl;
}

int main()
{
    std::barrier<> b(THREAD_COUNT);
    thread t[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; i++) {
        t[i] = thread(worker, i, ref(b));
    }
    for (int i = 0; i < THREAD_COUNT; i++) {
        t[i].join();
    }
    cout << "All workers finished" << endl;
    return 0;
}

5、原子操作(atomic operation):

原子操作是一种不可分割的操作,可以确保在任何时候只有一个线程能够访问共享资源。原子操作通常用于实现计数器或其他需要原子访问的共享资源。
这些方法可以单独或结合使用,以实现有效的线程同步和并发控制。

例子:

#include <atomic>
#include <thread>
#include <iostream>

// 原子操作则是一种特殊的数据类型,提供了一种高效的线程安全机制,可以保证共享变量的读写操作是原子性的,
// 不会出现数据竞争和不一致的情况。原子操作提供了多种操作符和方法,如原子增加、原子减少、原子比较交换等,
// 可以用来实现线程安全的计数器、标志位等数据结构。需要注意的是,原子操作虽然可以保证线程安全,
// 但并不能消除所有的并发问题,例如死锁、饥饿等情况。
// 因此,相比于全局变量,原子操作提供了一种更加高效和简单的线程安全机制,可以用于实现多线程编程中的一些共享数据结构。


#include <atomic>
#include <thread>
#include <iostream>

class Counter {
public:
  Counter() : value_(0) {}  // 初始化原子变量

  void increment() {
    for (int i = 0; i < 1000; ++i) {
      value_++;  // 对原子变量进行自增操作
    }
  }

  int getValue() const {
    return value_.load();  // 获取原子变量的值
  }

private:
  std::atomic<int> value_;  // 原子变量作为类的成员变量
};

int main() {
  Counter counter;

  std::thread t1(&Counter::increment, &counter);
  std::thread t2(&Counter::increment, &counter);
  t1.join();
  t2.join();

  std::cout << "Counter value: " << counter.getValue() << std::endl;
  return 0;
}

控制线程的顺序执行有以下几种方法:

1、使用互斥锁(mutex)和条件变量(condition variable)来实现同步控制。通过使用互斥锁和条件变量,可以在多个线程之间建立同步机制,以确保它们在适当的时候按照所需的顺序执行。

2、使用信号量(semaphore)来实现同步控制。信号量是一种计数器,可以用来控制对共享资源的访问。通过对信号量进行操作,可以实现多个线程之间的同步。

3、使用屏障(barrier)来实现同步控制。屏障是一种同步机制,它可以确保多个线程在某个点上同步执行。当多个线程都到达屏障时,它们被暂停,直到所有线程都到达该屏障,然后所有线程一起继续执行。

4、使用线程池(thread pool)来控制线程的顺序执行。线程池是一种管理多个线程的技术,它可以分配和管理线程,以便它们按照指定的顺序执行任务。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起风就扬帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值