文章目录
- 原子操作atomic
- 原子操作的相关函数
- 原子操作的特点
- “平凡的”与“合格的”
原子操作atomic
前面我们介绍了互斥锁等一系列多线程相关操作,这里我们来说下原子操作atomic。
可以理解为原子变量就是将上面的操作进行了整合的一个全新变量,但是实际上它的原理和互斥锁不一样,这关系到操作系统的底层,我也不了解。
原子操作的相关函数
方法 | 作用 |
---|---|
is_lock_free | 检查原子对象是否免锁 |
load | 原子地获得原子对象的值 |
exchange | 原子地替换原子对象的值并获得它先前持有的值 |
compare_exchange_weak/compare_exchange_strong | 原子地比较原子对象与非原子实参的值,相等时进行原子交换,不相等时进行原子加载 |
除此之外,它还有写特化成员函数,我看了下好像不是很常用,就用到的时候再补充吧。这些函数中,用得多的可能就是load()和compare_exchange_weak(strong)()了。
原子操作的特点
这里就说说原子操作的特点吧:
- 原子性:原子操作是不可分割的操作,要么完成整个操作,要么不进行操作。在多线程环境中,原子操作保证了共享数据的完整性
- 线程安全:原子操作提供了线程安全的操作,多个线程可以同时执行原子操作而不会导致数据竞争或不一致的结果
- 内存顺序:原子操作可以指定内存顺序,即操作的内存访问顺序。C++标准库提供了不同的内存顺序选项,可以控制原子操作的可见性和排序
- 轻量级:原子操作是一种轻量级的同步机制,相比于锁或互斥量等其他同步机制,原子操作的开销更小
- 支持不同的数据类型:C++原子操作可以用于不同的数据类型,包括整型、指针和标量类型等
原子性和线程安全这两点是有关联的,原子性就保证了原子操作一定是线程安全的;但是内存顺序这点我不是很理解,应该需要计算机组成原理或者OS的相关知识。
在这里,我想要强调的是第三点:支持不同的数据类型。
在学完条件变量之后,我写了一段简单的消息队列:
#include <iostream>
#include <thread>
#include <memory>
#include <string>
#include <condition_variable>
#include <list>
#include <atomic>
std::mutex mtx;
std::condition_variable cv;
std::list<std::string> msg;
// 读取数据
void read_thread(){
while(true){
std::unique_lock<std::mutex> lock(mtx);
// 阻塞等待消息(并且解锁)
// 有消息再执行,没消息不执行
cv.wait(lock,[&](){ return !msg.empty(); });
// 获取到互斥锁
std::cout << "收到消息,解析中:" << std::endl;
// 将数据从队列中取出
std::cout << msg.front() << std::endl;
msg.pop_front();
}
}
// 写入数据
void write_thread(){
std::cout << "请输入需要发送的数据:" << std::endl;
std::string input;
while(true){
if(std::cin >> input){
std::unique_lock<std::mutex> lock(mtx);
// 将数据放入队列
msg.push_back(input);
std::cout << "数据成功输入" << std::endl;
// 通知read线程,有消息可以接收
cv.notify_all();
}
}
}
int main(){
std::thread write_(write_thread);
// 后台运行
write_.detach();
std::thread read_(read_thread);
// 后台运行
read_.detach();
// 阻塞主线程
while(true);
return 0;
}
其中的重点就是条件变量和互斥锁,当我学到原子操作的时候我就想:==我能不能将消息队列直接设定为原子操作呢?==于是就有了如下代码:
#include <atomic>
#include <list>
#include <thread>
#include <iostream>
#include <string>
#include <condition_variable>
std::atomic<std::list<std::string>> msg;
std::condition_variable cv;
std::mutex mtx;
void write_thread(){
std::string input;
while(true){
if(std::cin >> input){
std::cout << "请输入消息:" << std::endl;
msg.load().emplace_back(input);
cv.notify_one();
}
}
}
void read_thread(){
while(true){
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&](){ return !msg.load().empty(); });
std::cout << "收到消息,正在解析:" << std::endl;
std::cout << msg.load().front() << std::endl;
msg.load().pop_front();
}
}
int main(){
std::thread write_(write_thread);
write_.detach();
std::thread read_(read_thread);
read_.detach();
while(true);
return 0;
}
程序在写的时候没有报错提示,但是它跑不起来,有一个全新的报错信息:
至少我没看懂,同时终端给出了一个函数报错信息:
于是我就去查询C++参考手册,其中提到:
其他都差不多能看懂,主要问题就是在这个可平凡复制上,这个我也从来没有听说过,但是在其中出现了之前的报错函数。
于是我就顺着往下查,找到了关于可平凡复制类的说明:
根据这个说明,我才知道这个报错是因为std::string和std::list都不是可平凡复制类,所以出错了。
“平凡的”与“合格的”
在上面提供的信息中,我们其实还是不能够完全理解它是为什么,因为我们还不知道什么是平凡的:
![[…/图片资源/C++/平凡的与合格的.png]]
从这里对“平凡的”的定义,我理解为:”平凡的“就是编译器默认提供的!,例如:C语言中的struct,我们是不能够去编写它的构造函数和析构函数的,相关的操作只有才C++中才支持。因此由编译器提供构造函数和析构函数struct类一定是“平凡的”。相对应的,C++中,由编译器提供构造函数和析构函数class类一般来说也是“平凡的“。
并且,在C++中,提供了相应的函数用来评定一个类是否能够使用原子操作,也就是主模板中提到的五个函数:
- std::is_trivially_copyable<T>::value
- std::is_copy_constructible<T>::value
- std::is_move_constructible<T>::value
- std::is_copy_assignable<T>::value
- std::is_move_assignable<T>::value
若是这五个函数的返回值有一个为false,就不能够使用原子操作。
对于原子操作,真的没有什么太多的内容,它就跟普通的变量差不了太多,只是它是原子的,具有线程安全的特性。