
您好,我是昊天,国内某头部音频公司的C++主程,多年的音视频开发经验,熟悉Qt、FFmpeg、OpenGL。如果你对这些方面感兴趣,欢迎关注我的公众号,一起学习,一起进步。
各位读者朋友,我的上一篇文章,在发文64小时内,获得8000多阅读、近600转发,推荐大家看看:回调函数实战解读:从 C/C++ 到现代 C++ 实现方案
在上一篇文章的评论区中有位读者朋友希望多出点线程安全的模块

所以今天这篇文章从What、Why、How三个方面阐述线程安全,并提出了一些实战指南(Do)。
如下为一个简化的可能会触发线程不安全的例子
#include<iostream>
#include<thread>
int main(){
int a=0;
std::thread t1([&a](){
for(int i =0;i<10000;i++){
a++;
}
});
std::thread t2([&a](){
for(int i =0;i<10000;i++){
a++;
}
});
if(t1.joinable()){
t1.join();
}
if(t2.joinable()){
t2.join();
}
std::cout<<"after thread process a= "<<a<<" \n";
return0;
}
//输出
//after thread process a= 16642
从上面的代码中可以看到,a的期望结果为20000,但是实际输出的结果却是16642,这就是线程不安全的表现。
还是如上的例子,只是将两个线程访问的变量分别设置为a和b,即t1访问a,t2访问b,
#include<iostream>
#include<thread>
int main()
{
int a = 0, b = 0;
std::thread t1([&a]() {
for (int i = 0; i < 1000; i++ ) {
a++;
}
});
std::thread t2([&b]() {
for (int i = 0; i < 1000; i++ ) {
b++;
}
});
//与上文相同,省略了join操作
std::cout << "after t1 thread a= " << a << " \n";
std::cout << "after t2 thread b= " << b << "\n";
return0;
}
//输出
//after t1 thread a= 1000
//after t2 thread b= 1000
通过输出可以看到,a和b的值都为1000,即此时并不存在线程安全问题。对比如上两个代码,可以发现,当两个线程访问同一个变量时,才有可能出现线程安全问题。 同理,如果我们将a、b变量更改为std::vector<int> vec(2,0),t1访问vec[0],t2访问vec[1],此时也不会出现线程安全问题。【代码示例略】
综合本节和上节中的3个例子,可以得出结论:
std::vector<int>并不存在线程安全问题,std::map、std::set、std::queue同理。而上节例子中的int变量却存在线程安全问题。前文分析了产生线程安全问题的根本原因为:多个线程同时访问同一变量时,某个线程对于变量的修改不能被其他线程立即看到。因此,要避免线程安全问题,可以从两个方面入手:
具体方法可以有:
原子变量是C++11中引入的用于解决数据线程安全问题的工具,其借助硬件层的支持,实现了三个“绝活”:
原子变量的使用又是非常简单的,如第一节中的int a修改为std::atomic<int> a即可避免数据安全问题。
关于原子变量,写过很多文章,可通过如下链接查看:
从导致线程安全的原因入手,避免在多线程环境下访问同一变量是可以避免线程安全问题的。具体的方法可以有:
std::mutex:互斥锁,最简单的锁,同一时刻只有一个线程可以访问被保护的变量。std::recursive_mutex:递归互斥锁,允许同一个线程多次加锁,但必须加锁多少次,就要解锁多少次。std::timed_mutex:定时互斥锁,允许在一段时间内加锁,如果超过时间则加锁失败。std::recursive_timed_mutex:递归定时互斥锁,允许在一段时间内加锁,如果超过时间则加锁失败,允许同一个线程多次加锁,但必须加锁多少次,就要解锁多少次。std::shared_mutex:共享互斥锁,允许多个线程同时读,但同一时刻只有一个线程可以写。std::lock_guard:RAII操作,在构造时加锁,在析构时解锁。std::unique_lock:RAII操作,在构造时加锁,在析构时解锁;但允许在析构前解锁,允许解锁后加锁,允许加锁后再解锁。std::shared_lock:RAII操作,在构造时加锁,在析构时解锁;但允许在析构前解锁,允许解锁后加锁,允许加锁后再解锁。此时我想提一句:条件变量并不能避免数据安全问题,条件变量只是用于线程间的同步,避免线程间的竞争。
无锁编程基于原子变量实现,在之前的章节中做过详细的介绍,这里不再赘述。见如下链接:无锁数据结构
std::lock可以一次性加锁多个锁,避免了死锁。随着多核时代的到来,多线程编程已经成为了程序员必备的技能,进而的,线程安全也成为了程序员必须掌握的知识。本文从是什么、为什么、怎么做三个方面介绍了线程安全,也提出了一些实操建议,希望对大家有所帮助。