c++ 并发算法使用注意事项中线程安全性、数据竞争问题
线程安全性与数据竞争
在多线程编程中,线程安全性和数据竞争是两个非常重要的概念。如果处理不当,可能会导致程序行为异常或崩溃。以下是对这两个概念的详细说明,并通过程序实例加以演示。
1. 线程安全性
线程安全性指的是在多线程环境下,程序或函数能够正确地处理共享数据,而不会导致数据不一致或程序崩溃。
示例:非线程安全的 std::for_each
在以下代码中,多个线程同时修改共享变量 sum,导致数据不一致。
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> vec(1000, 1); // 1000 个 1
int sum = 0;
// 使用并行策略累加 vec 的元素
std::for_each(std::execution::par, vec.begin(), vec.end(), [&](int n) {
sum += n; // 非线程安全操作
});
std::cout << "Sum: " << sum << std::endl; // 结果可能小于 1000
return 0;
}
问题分析:
- 多个线程同时修改
sum,导致数据竞争。 sum += n不是原子操作,可能会被多个线程同时执行,导致部分修改丢失。
2. 数据竞争
数据竞争指的是多个线程同时访问共享数据,且至少有一个线程在修改数据,导致程序行为不可预测。
示例:数据竞争导致程序崩溃
在以下代码中,多个线程同时向 std::vector 中添加元素,导致数据竞争。
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> vec;
// 使用并行策略向 vec 中添加元素
std::for_each(std::execution::par, vec.begin(), vec.end(), [&](int) {
vec.push_back(1); // 非线程安全操作
});
std::cout << "Vector size: " << vec.size() << std::endl; // 结果不可预测
return 0;
}
问题分析:
- 多个线程同时调用
vec.push_back(1),导致std::vector内部状态不一致。 - 可能导致程序崩溃或数据丢失。
3. 如何解决线程安全性和数据竞争
方法 1:使用互斥锁(std::mutex)
通过互斥锁保护共享数据,确保同一时间只有一个线程可以访问。
示例:使用 std::mutex 保护共享变量
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <mutex>
int main() {
std::vector<int> vec(1000, 1); // 1000 个 1
int sum = 0;
std::mutex mtx;
// 使用并行策略累加 vec 的元素
std::for_each(std::execution::par, vec.begin(), vec.end(), [&](int n) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
sum += n; // 线程安全操作
});
std::cout << "Sum: " << sum << std::endl; // 输出: 1000
return 0;
}
改进点:
- 使用
std::mutex保护sum,确保同一时间只有一个线程可以修改sum。
方法 2:使用原子操作(std::atomic)
通过原子操作确保对共享变量的修改是原子的。
示例:使用 std::atomic 保护共享变量
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <atomic>
int main() {
std::vector<int> vec(1000, 1); // 1000 个 1
std::atomic<int> sum = 0;
// 使用并行策略累加 vec 的元素
std::for_each(std::execution::par, vec.begin(), vec.end(), [&](int n) {
sum += n; // 原子操作
});
std::cout << "Sum: " << sum << std::endl; // 输出: 1000
return 0;
}
改进点:
- 使用
std::atomic确保sum += n是原子操作,无需显式加锁。
方法 3:避免共享数据
通过设计避免多个线程访问共享数据,例如使用线程本地存储(Thread Local Storage, TLS)。
示例:使用线程本地存储
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>
#include <mutex>
thread_local int local_sum = 0; // 每个线程有自己的 local_sum
int main() {
std::vector<int> vec(1000, 1); // 1000 个 1
int sum = 0;
std::mutex mtx;
// 使用并行策略累加 vec 的元素
std::for_each(std::execution::par, vec.begin(), vec.end(), [&](int n) {
local_sum += n; // 每个线程累加自己的 local_sum
});
// 将每个线程的 local_sum 累加到 sum
std::lock_guard<std::mutex> lock(mtx);
sum += local_sum;
std::cout << "Sum: " << sum << std::endl; // 输出: 1000
return 0;
}
改进点:
- 每个线程有自己的
local_sum,避免共享数据。 - 最后将各个线程的结果累加到
sum。
4. 总结
- 线程安全性:确保多线程环境下共享数据的正确性。
- 数据竞争:多个线程同时访问共享数据可能导致程序行为异常。
- 解决方法:
- 使用互斥锁(
std::mutex)保护共享数据。 - 使用原子操作(
std::atomic)确保操作的原子性。 - 避免共享数据,例如使用线程本地存储。
- 使用互斥锁(
通过合理使用同步机制和设计,可以避免线程安全性和数据竞争问题,确保多线程程序的正确性和性能。

被折叠的 条评论
为什么被折叠?



