在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)来控制线程的顺序执行。线程池是一种管理多个线程的技术,它可以分配和管理线程,以便它们按照指定的顺序执行任务。