C++多线程编程(2) 条件变量与原子操作

条件变量

条件变量是c++11 提供的另一种用于等待的同步机制,它能阻塞一个或者多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程,条件变量需要和互斥量配合使用,C++11提供两种互斥变量:

1. condition_variable: 需要配合std::unique_lock<std::mutex>进行wait操作

2. condition_variable_any: 和任意带有lock,unlock的mutext配合使用。更加灵活,但是效率较低。

条件变量的使用过程如下:
1. 拥有条件变量的线程获取互斥量

2.循环检查某个条件, 如果条件不满足,则阻塞直到条件满足。如果条件满足,则向下执行

3. 某个线程满足条件执行完之后调用notify_one或者notify_all唤醒一个或者所有的等待线程。

介绍一个例子,用条件变量实现一个同步队列,在线程之间读取数据的时候,同步队列可以作为一个线程安全的数据共享区。

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <thread>
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <list>
// 定义模板类 同步队列
template<typename T>
class SyncQueue
{
private:
// 数据域
std::list<T> m_queue;
std::mutex mutex;
std::condition_variable_any m_notEmpty;
std::condition_variable_any m_notFull;
int max_size; // 同步队列最大的容量
public:
// 构造函数
SyncQueue(int max_size)
{
this->max_size = max_size;
}
bool isFull() const
{
return m_queue.size() == max_size;
}
bool isEmpty() const
{
return m_queue.empty();
}
void put(T& x) // 插入元素
{
std::lock_guard<std::mutex> locker(mutex); // lock_guard使用RAII技术
while (isFull()) // 判断条件 队列为满的时候
{
std::cout << "缓冲区已满,需要等待...." << std::endl;
m_notEmpty.wait(mutex); // 拥有条件变量的线程获取互斥量 循环检查isFull()条件 如果是真,则会阻塞线程
}
// 继续向下执行
m_queue.push_back(x);
m_notEmpty.notify_one(); // 唤醒其他线程
}
T take()
{
std::lock_guard<std::mutex> locker(mutex);
while (isEmpty()) // 判断条件 队列为空
{
std::cout << "缓冲区空了,需要等待...." << std::endl;
m_notFull.wait(mutex); // 阻塞线程
}
T elem = m_queue.front();
m_queue.pop_front();
m_notFull.notify_one(); // 唤醒其他线程
return elem;
}
bool Empty()
{
std::lock_guard<std::mutex> locker(mutex);
return m_queue.empty();
}
bool Full()
{
std::lock_guard<std::mutex> locker(mutex);
return m_queue.size() == max_size;
}
int Count()
{
std::lock_guard<std::mutex> locker(mutex);
return m_queue.size();
}
};
SyncQueue<int> queue(2000); // 队列
void insert(int ins)
{
while (true)
{
queue.put(ins);
std::cout << "Thread" << std::this_thread::get_id() << "insert the data" << ins << std::endl;
}
}
void take_out()
{
while (true)
{
auto elem = queue.take();
std::cout << "Thread " << std::this_thread::get_id() << "take the data" << elem << std::endl;
}
}
int main()
{
std::thread t1(insert, 3);
std::thread t2(insert, 5);
std::thread t3(take_out);
t1.join();
t2.join();
t3.join();
return 0;
}

不同的线程实现对队列的插入数据和拿出数据的操作。这个同步队列在没有满的情况下可以插入数据,如果满了,则会调用条件变量m_notEmpty阻塞线程,待消费线程取出数据之后发送一个未满的通知,前面阻塞的线程就会被唤醒继续往下执行。当同步队列为空的时候,就不能取数据,调用m_notFull阻塞线程,等待出入数据的线程发送一个队列非空的信息,才继续往下执行,同步队列的工作过程如上所示。

在上述代码中,有一块地方需要解释清除:

T take()
{
std::lock_guard<std::mutex> locker(mutex);
while (isEmpty()) // 判断条件 队列为空
{
std::cout << "缓冲区空了,需要等待...." << std::endl;
m_notFull.wait(mutex); // 阻塞线程
}
T elem = m_queue.front();
m_queue.pop_front();
m_notFull.notify_one(); // 唤醒其他线程
return elem;
}

以take方法为例,首先,调用该方法的时候,会获取互斥量,然后进行条件判断,如果队列为空,会执行一个wait操作,wait操作的过程就是先释放互斥量,并将线程设置为waiting状态,继续等待,此时互斥量被释放,则其他的线程可以对同步队列进行操作(插入或者删除数据),当线程非空时,处于waiting状态的线程会被唤醒,重新获取互斥量mutex,继续执行后续的操作。

unique_lock可以自由的释放mutex,lock_guard需要等到lock_guard变量的生命周期结束的时候才能释放,所以在这两者配合condition_variable使用的时候,需要注意:

1. wait会提前释放mutex,但是lock_guard变量只有在生命周期结束的时候才能释放mutex, 那么在lock_guard和condition_variable配合使用的时候貌似会出现矛盾,即wait提前释放mutex,那么在lock_guard变量生命周期结束的时候,没有可以释放的mutex。但是实际这样做并不会出现问题,因为wai提前释放锁之后处于等待状态,但是在被唤醒的时候,会重新获得锁。

上述的wait还有一个重载的方法,可以使得程序更加的简洁,这两种写法的作用是等价的: lambda表达式来判断

T take()
{
// 一种等价的写法
std::lock_guard<std::mutex> locker(mutex);
m_notFull.wait(mutex, [this] {return isEmpty(); });
T elem = m_queue.front();
m_queue.pop_front();
m_notFull.notify_one(); // 唤醒其他线程
return elem;
}

原子操作:

C++中原子操作的“原子”,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个资源的时候,能够确保左右其他的线程都不在同一时间内访问相同的资源,也就是它确保了在同一时刻只有唯一的一个线程对这个资源进行访问。类似于互斥对象对共享资源的访问保护,但是原子操作更加接近底层,因此它的效率也就更高。

在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。(来自博客:https://blog.csdn.net/yockie/article/details/8838686 )

举个例子:数值累加的程序

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <ctime>
#include <chrono>
#include <thread>
#include "Timer.h" // 计时器
#include <vector>
long long cnt = 0; // 全局变量
void counter()
{
std::cout << std::this_thread::get_id() << " is running" << std::endl;
for (int i = 0; i < 10000; i++)
{
cnt++;
}
}
int main()
{
vector<std::thread> threads;
Timer timer;
for (int i = 0; i < 5; i++)
{
threads.push_back(std::thread(counter));
}
for (auto& thr : threads)
{
thr.join();
}
std::cout << "Time elapsed is " << timer.elapsed_nanoseconds() << " nano seconds" << std::endl;
std::cout << "count = " << cnt << std::endl;
return 0;
}

输出结果:

在没有加锁的情况下,输出的结果不正确

加锁之后:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <ctime>
#include <chrono>
#include <thread>
#include "Timer.h" // 计时器
#include <vector>
#include <mutex>
long long cnt = 0; // 全局变量
std::mutex g_lock;
void counter()
{
std::cout << std::this_thread::get_id() << " is running" << std::endl;
g_lock.lock();
for (int i = 0; i < 10000; i++)
{
cnt++;
}
g_lock.unlock();
}
int main()
{
vector<std::thread> threads;
Timer timer;
for (int i = 0; i < 5; i++)
{
threads.push_back(std::thread(counter));
}
for (auto& thr : threads)
{
thr.join();
}
std::cout << "Time elapsed is " << timer.elapsed_nanoseconds() << " nano seconds" << std::endl;
std::cout << "count = " << cnt << std::endl;
return 0;
}

输出结果正确:

利用原子变量解决上述问题:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <ctime>
#include <chrono>
#include <thread>
#include "Timer.h" // 计时器
#include <vector>
#include <mutex>
#include <atomic>
// long long cnt = 0; // 全局变量
std::atomic<long> cnt = 0;
void counter()
{
std::cout << std::this_thread::get_id() << " is running" << std::endl;
for (int i = 0; i < 10000; i++)
{
cnt++;
}
}
int main()
{
vector<std::thread> threads;
Timer timer;
for (int i = 0; i < 5; i++)
{
threads.push_back(std::thread(counter));
}
for (auto& thr : threads)
{
thr.join();
}
std::cout << "Time elapsed is " << timer.elapsed_nanoseconds() << " nano seconds" << std::endl;
std::cout << "count = " << cnt << std::endl;
return 0;
}

C++中 call_once 和 once_flag的使用:

如果需要在多线程的环境中使得某个函只被调用一次,例如,需要初始化某个对象,就可以使用std::call_once来保证函数在多线程环境中只被调用一次,使用call_once的时候需要一个once_flag作为传入的参数,用法如下:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <vector>
#include "Timer.h" // 计时器
#include <mutex>
std::once_flag flag, flag1;
void do_once() // 无参数的函数
{
std::cout << "called only once" << std::endl;
}
void printfx(int x) // 有参数的函数
{
std::cout << "you input " << x << std::endl;
}
void counter()
{
std::cout << "Thread " << std::this_thread::get_id() << " is running! " << std::endl;
// std::call_once(flag, []() {do_once(); });
std::call_once(flag, do_once); // 被调用的函数必须定义在counter()函数前面
std::call_once(flag1, printfx, 2);
}
int main()
{
vector<std::thread> threads;
Timer timer;
for (int i = 0; i < 5; i++)
{
threads.push_back(std::thread(counter));
}
for (auto& thr : threads)
{
thr.join();
}
std::cout << "Time elapsed is " << timer.elapsed_nanoseconds() << " nano seconds" << std::endl;
return 0;
}

std::call_once的使用方法比较灵活,可以传入有参函数,无参函数和lambda函数以及对象的方法等。

posted @   Alpha205  阅读(278)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示