加法运算的 C++ 线程安全
C++ Thread Safety For Addition Operation
假设您正在从 2 个不同的线程更新共享数据结构,其中操作顺序无关紧要。所以所需要的只是来自两个线程的操作的正确结果。
下面的代码在我的系统上运行良好,但我认为这不是线程安全的。
#include <thread>
#include <iostream>
#include <vector>
// A vector of [100] which all have 0 as their initial values.
std::vector<int> common(100, 0);
void add10(std::vector<int> ¶m){
for (std::vector<int>::iterator it = param.begin();
it != param.end(); ++it){
*it += 10;
}
}
void add100(std::vector<int> ¶m){
for (std::vector<int>::iterator it = param.begin();
it != param.end(); ++it){
*it += 100;
}
}
int main(int argc, char *argv[]) {
// Print vector
for (std::vector<int>::iterator it = common.begin();
it != common.end(); ++it){
std::cout << *it << std::endl;
}
std::cout << "==> Initial Vector" << std::endl;
std::thread t1(add10, std::ref(common));
std::thread t2(add100, std::ref(common));
t1.join();
t2.join();
// Print vector again
for (std::vector<int>::iterator it = common.begin();
it != common.end(); ++it){
std::cout << *it << std::endl;
}
std::cout << "==> Resulting Vector" << std::endl;
}
我在这里关心的是数据竞争;
- 假设 t1 正在迭代第 10 个元素。从迭代器中获取值为 0。
- 同时,t2 正在迭代第 10 个元素,因此它们都读入了值 0。
- 然后他们做加法,分别是10和100。
那么在分配回向量时会不会出现数据竞争?
编辑:
对于STL数据结构,事实证明你可以像std::vector<std::atomic<int>>
一样使用它
是的,因为这些操作不是原子的。您使用迭代器获取一个值,向其添加一些内容并将该值存储回去——这背后有相当多的机器代码。另一个线程可以随时插入并替换您的值。
您必须确保此代码的线程安全。
您可以反汇编二进制文件或生成汇编代码,以查看这是如何在机器级别执行的。在 GCC 中,您可以使用 -S
(注意:大写 S)开关来生成程序集列表。
如果一个线程写入一个对象,而另一个线程在没有适当同步的情况下访问(读取或写入)该对象,则会出现数据争用。如果程序包含数据竞争,则其结果是不确定的。您的程序不包含同步,但它确实并发访问共享对象,即它会导致未定义的行为。
我发现 benign data races 上的文章非常有趣:即使没有得到正确的结果是可以接受的,但事情还是会出错。
毫无疑问存在数据竞争,事实上,在 运行 你的代码两次之后,我发现结果向量的一些条目是 10,而不是 110,因为它们应该是。您可以通过 helgrind 运行 您的可执行文件立即看到这一点,即 valgrind --tool=helgrind ./race
,它会告诉您确切的问题所在:
==3176== Possible data race during read of size 4 at 0x5C41040 by thread #3
==3176== Locks held: none
==3176== at 0x4010CD: add100(std::vector<int, std::allocator<int> >&) (race.cc:19)
==3176== This conflicts with a previous write of size 4 by thread #2
==3176== Locks held: none
==3176== at 0x40106C: add10(std::vector<int, std::allocator<int> >&) (race.cc:12)
使用std::atomic<int>
,而不是int
s,必要时处理delete
的原子复制构造函数的问题。
假设您正在从 2 个不同的线程更新共享数据结构,其中操作顺序无关紧要。所以所需要的只是来自两个线程的操作的正确结果。
下面的代码在我的系统上运行良好,但我认为这不是线程安全的。
#include <thread>
#include <iostream>
#include <vector>
// A vector of [100] which all have 0 as their initial values.
std::vector<int> common(100, 0);
void add10(std::vector<int> ¶m){
for (std::vector<int>::iterator it = param.begin();
it != param.end(); ++it){
*it += 10;
}
}
void add100(std::vector<int> ¶m){
for (std::vector<int>::iterator it = param.begin();
it != param.end(); ++it){
*it += 100;
}
}
int main(int argc, char *argv[]) {
// Print vector
for (std::vector<int>::iterator it = common.begin();
it != common.end(); ++it){
std::cout << *it << std::endl;
}
std::cout << "==> Initial Vector" << std::endl;
std::thread t1(add10, std::ref(common));
std::thread t2(add100, std::ref(common));
t1.join();
t2.join();
// Print vector again
for (std::vector<int>::iterator it = common.begin();
it != common.end(); ++it){
std::cout << *it << std::endl;
}
std::cout << "==> Resulting Vector" << std::endl;
}
我在这里关心的是数据竞争;
- 假设 t1 正在迭代第 10 个元素。从迭代器中获取值为 0。
- 同时,t2 正在迭代第 10 个元素,因此它们都读入了值 0。
- 然后他们做加法,分别是10和100。
那么在分配回向量时会不会出现数据竞争?
编辑:
对于STL数据结构,事实证明你可以像std::vector<std::atomic<int>>
是的,因为这些操作不是原子的。您使用迭代器获取一个值,向其添加一些内容并将该值存储回去——这背后有相当多的机器代码。另一个线程可以随时插入并替换您的值。
您必须确保此代码的线程安全。
您可以反汇编二进制文件或生成汇编代码,以查看这是如何在机器级别执行的。在 GCC 中,您可以使用 -S
(注意:大写 S)开关来生成程序集列表。
如果一个线程写入一个对象,而另一个线程在没有适当同步的情况下访问(读取或写入)该对象,则会出现数据争用。如果程序包含数据竞争,则其结果是不确定的。您的程序不包含同步,但它确实并发访问共享对象,即它会导致未定义的行为。
我发现 benign data races 上的文章非常有趣:即使没有得到正确的结果是可以接受的,但事情还是会出错。
毫无疑问存在数据竞争,事实上,在 运行 你的代码两次之后,我发现结果向量的一些条目是 10,而不是 110,因为它们应该是。您可以通过 helgrind 运行 您的可执行文件立即看到这一点,即 valgrind --tool=helgrind ./race
,它会告诉您确切的问题所在:
==3176== Possible data race during read of size 4 at 0x5C41040 by thread #3
==3176== Locks held: none
==3176== at 0x4010CD: add100(std::vector<int, std::allocator<int> >&) (race.cc:19)
==3176== This conflicts with a previous write of size 4 by thread #2
==3176== Locks held: none
==3176== at 0x40106C: add10(std::vector<int, std::allocator<int> >&) (race.cc:12)
使用std::atomic<int>
,而不是int
s,必要时处理delete
的原子复制构造函数的问题。