为什么在单独的线程中调用成员函数会导致 non-deterministic 行为?
Why does calling a member member function in a separate thread result in non-deterministic behaviour?
我将我的问题分解为可重现的问题。在下面的代码中我们有一个 class printer
。 printer::run
将打印 printer
的 id_
。 printer::spawn
将为您介绍新打印机。
在 main
中,我生成了 10 台打印机。创建的线程将在 main
结束时加入。我的期望是,每次我们 运行 这个(这是标题中的 "deterministic behavior" )时,我们都会按某种顺序获得从 0 到 9 的数字。不是这种情况。我们有时会得到其他数字。这是为什么?
#include <thread>
#include <iostream>
#include <vector>
class printer
{
public:
printer(int i) : i_(i){};
void run()
{
std::cout << i_ << '\n';
}
std::shared_ptr<std::thread> spawn()
{
return std::make_shared<std::thread>([=]() { run(); });
}
private:
int i_;
};
int main()
{
std::vector<std::shared_ptr<std::thread>> vec;
for (int i = 0; i < 10; ++i)
{
printer p{i};
vec.push_back(p.spawn());
}
for (auto a : vec)
{
a->join();
}
return 0;
}
关于我的期望的一些想法:
我猜想我们在调用 spawn 时复制了当前 printer
的状态,因为我们通过值 (lambda) 捕获状态,而不是通过引用。
即使我们通过引用捕获,我们仍然将状态复制到新线程。
即使没有发生这种情况,每个线程都有自己的 printer
。我错过了什么?
一些示例输出:
(为简洁起见,我将所有数字放在一行中)
1, 3, 4, 2, 5, 6, 7, 8, 9, -116618192
1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144
1, 2, 33, , 6, 6, 7, 9, 9, 1344437296
1, 43, 2, 6, , 6, 7, 8, 9, -1194894256
有些输出没有全部 10 个数字...
这是因为您在创建线程的循环中创建和销毁了一个打印机对象。当循环进入下一次迭代时,上一次迭代创建的打印机已经被销毁,你创建的引用它的线程将引用这个被销毁的对象。这是未定义的行为。
基本上您的所有线程都将使用相同的 printer
对象。在某些时候,它占用的内存在用于保存其他数据时会被覆盖。
您可能想要创建一个打印机数组,以便可以将不同的打印机传递给每个线程。
这里
{
printer p{i};
vec.push_back(p.spawn());
}
对象 p
的生命周期结束于 }
。因此,您推入向量的 lambda 将尝试调用不再存在的对象的成员函数。您的代码有未定义的行为,看到正确的输出只是巧合。
我将我的问题分解为可重现的问题。在下面的代码中我们有一个 class printer
。 printer::run
将打印 printer
的 id_
。 printer::spawn
将为您介绍新打印机。
在 main
中,我生成了 10 台打印机。创建的线程将在 main
结束时加入。我的期望是,每次我们 运行 这个(这是标题中的 "deterministic behavior" )时,我们都会按某种顺序获得从 0 到 9 的数字。不是这种情况。我们有时会得到其他数字。这是为什么?
#include <thread>
#include <iostream>
#include <vector>
class printer
{
public:
printer(int i) : i_(i){};
void run()
{
std::cout << i_ << '\n';
}
std::shared_ptr<std::thread> spawn()
{
return std::make_shared<std::thread>([=]() { run(); });
}
private:
int i_;
};
int main()
{
std::vector<std::shared_ptr<std::thread>> vec;
for (int i = 0; i < 10; ++i)
{
printer p{i};
vec.push_back(p.spawn());
}
for (auto a : vec)
{
a->join();
}
return 0;
}
关于我的期望的一些想法:
我猜想我们在调用 spawn 时复制了当前 printer
的状态,因为我们通过值 (lambda) 捕获状态,而不是通过引用。
即使我们通过引用捕获,我们仍然将状态复制到新线程。
即使没有发生这种情况,每个线程都有自己的 printer
。我错过了什么?
一些示例输出:
(为简洁起见,我将所有数字放在一行中)
1, 3, 4, 2, 5, 6, 7, 8, 9, -116618192
1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144, 1513168144
1, 2, 33, , 6, 6, 7, 9, 9, 1344437296
1, 43, 2, 6, , 6, 7, 8, 9, -1194894256
有些输出没有全部 10 个数字...
这是因为您在创建线程的循环中创建和销毁了一个打印机对象。当循环进入下一次迭代时,上一次迭代创建的打印机已经被销毁,你创建的引用它的线程将引用这个被销毁的对象。这是未定义的行为。
基本上您的所有线程都将使用相同的 printer
对象。在某些时候,它占用的内存在用于保存其他数据时会被覆盖。
您可能想要创建一个打印机数组,以便可以将不同的打印机传递给每个线程。
这里
{
printer p{i};
vec.push_back(p.spawn());
}
对象 p
的生命周期结束于 }
。因此,您推入向量的 lambda 将尝试调用不再存在的对象的成员函数。您的代码有未定义的行为,看到正确的输出只是巧合。