在析构函数中调用 join() 时的不一致行为
Inconsistent behaviour when calling join() in the destructor
考虑以下代码,我在其中声明了一个简单的 class 用于执行 asynchronous/threaded 操作:
#include <chrono>
#include <thread>
#include <mutex>
#include <future>
#include <iostream>
using namespace std::chrono_literals;
class basic_executor {
public:
basic_executor() {
_mx.lock();
printf("Ctor @%p\n", this);
_mx.unlock();
}
virtual ~basic_executor() {
_mx.lock();
printf("Dtor @%p\n", this);
_mx.unlock();
if (_thread.joinable()) {
_thread.join();
_mx.lock();
printf("Joined thread @%p\n", this);
_mx.unlock();
}
}
// sync call
void run() {
start();
execute();
stop();
}
// async call
void launch(bool detach = false) {
// create packaged task
std::packaged_task< void() > task([this] {
start();
execute();
stop();
});
// assign future object to function return
_done = task.get_future();
// launch function on a separate thread
_thread = std::thread(std::move(task));
// detach them from main thread in order to avoid waiting for them
if (detach == true) {
_thread.detach();
}
}
// blocking wait for async (detached/joinable)
void wait() const {
_done.wait();
}
protected:
virtual void start() { /* for derived types to implement */ }
virtual void stop() { /* for derived types to implement */ }
virtual void execute() { /* for derived types to implement */ }
std::mutex _mx;
std::thread _thread;
std::future< void > _done;
};
并使用我从中派生的以下应用程序示例来创建两个记录器对象,这些对象在特定时间范围内进行虚拟打印:
class logger : public basic_executor {
public:
logger() { /* ... */}
~logger() {
_mx.lock();
std::cout << "logger destructor " << std::endl;
_mx.unlock();
}
void execute() override {
std::this_thread::sleep_for(1s);
for (int i = 0; i < 10; ++i) {
_mx.lock();
printf("L1: I am printing something\n");
_mx.unlock();
std::this_thread::sleep_for(1s);
}
}
void stop() override {
_mx.lock();
printf("L1: I am done!\n");
_mx.unlock();
}
};
class logger2 : public basic_executor {
public:
logger2() { /* ... */}
~logger2() {
_mx.lock();
printf("logger2 destructor\n");
_mx.unlock();
}
void execute() override {
for (int i = 0; i < 10; ++i) {
_mx.lock();
printf("L2: I am ALSO printing something\n");
_mx.unlock();
std::this_thread::sleep_for(2s);
}
}
void stop() override {
_mx.lock();
printf("L2: I am done!\n");
_mx.unlock();
}
};
int main(int argc, char const *argv[]) {
/* code */
// printf("log:\n");
logger log1;
// printf("log1:\n");
logger2 log2;
printf("----------------------------------!\n");
log2.launch();
log1.launch();
// log1.wait();
// log2.wait();
printf("----------------------------------!\n");
return 0;
}
程序出现意外行为:
Ctor @0x7fff8b18c990
Ctor @0x7fff8b18c9e0
----------------------------------!
----------------------------------!
logger2 destructor
Dtor @0x7fff8b18c9e0
Joined thread @0x7fff8b18c9e0
logger destructor
Dtor @0x7fff8b18c990
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
Joined thread @0x7fff8b18c990
因为 偶尔,'log2' 对象在被销毁之前从未开始执行,或者对其析构函数的 'join()' 调用无限期挂起。发生这种情况有什么明显的原因吗?我到底错过了什么?
错误可能发生在日志记录 class 中。但是,对于未定义的行为,您没有任何保证,也没有任何一致结果的期望。到目前为止,您只观察到两个日志记录 classes 之一的相同错误。虽然我可以解释为什么,但实际上,这并不重要。该错误可能发生在任何一个对象上。让我们从这里开始:
_thread = std::thread(std::move(task));
你不会得到任何保证,即新的执行线程将在这段代码继续之前立即开始执行以下任何内容,returns 来自 launch()
:
std::packaged_task< void() > task([this] {
start();
execute();
stop();
});
大多数时候,实际上,这将在新的执行线程中很快 运行 启动。但你不能依赖它。 C++ 向您保证的是,在某个时刻 在 std::thread
完成构建一个新的执行线程后将启动 运行。它可能是立竿见影的。或者,它可能会晚几百毫秒,因为您的操作系统有更重要的事情要做。
您期望新的执行线程将始终“立即”开始执行,同时 std::thread
正在构建。那不是真的。毕竟,您可能 运行 是一个 CPU 核心,并且在构建 std::thread
对象之后,您将继续在同一个执行线程中执行后续操作,并且仅在稍后发生上下文切换,到新的执行线程。
同时:
launch
() returns.
父执行线程到达main()
末尾。
自动作用域中的所有对象都将被一一销毁。
在 C++ 中,当一个对象由一个 superclass 和一个 subclass 组成时,subclass 首先被销毁,然后是 superclass。这就是 C++ 的工作原理。
因此,logger
/logger2
subclass 的析构函数立即被调用并销毁其对象(只是 logger
/logger2
subclass).
现在 superclass 的析构函数被调用,以销毁 superclass。 ~basic_executor
开始做自己的事,耐心等待。
现在,最后,那个新的执行线程,还记得那个吗?你猜怎么着:它终于开始 运行,并勇敢地尝试执行 start()
、execute()
和 stop()
。或者它可能首先设法通过 start()
,但尚未达到 execute()
。但是由于实际的记录器 subclass 现在已经被销毁了,猜猜会发生什么?没有什么。它消失了。 subclass 没有了。它不再是。它加入了无形的合唱团。它渴望峡湾。是前潜艇class。没有 logger::execute
或 logger2::execute()
了。
考虑以下代码,我在其中声明了一个简单的 class 用于执行 asynchronous/threaded 操作:
#include <chrono>
#include <thread>
#include <mutex>
#include <future>
#include <iostream>
using namespace std::chrono_literals;
class basic_executor {
public:
basic_executor() {
_mx.lock();
printf("Ctor @%p\n", this);
_mx.unlock();
}
virtual ~basic_executor() {
_mx.lock();
printf("Dtor @%p\n", this);
_mx.unlock();
if (_thread.joinable()) {
_thread.join();
_mx.lock();
printf("Joined thread @%p\n", this);
_mx.unlock();
}
}
// sync call
void run() {
start();
execute();
stop();
}
// async call
void launch(bool detach = false) {
// create packaged task
std::packaged_task< void() > task([this] {
start();
execute();
stop();
});
// assign future object to function return
_done = task.get_future();
// launch function on a separate thread
_thread = std::thread(std::move(task));
// detach them from main thread in order to avoid waiting for them
if (detach == true) {
_thread.detach();
}
}
// blocking wait for async (detached/joinable)
void wait() const {
_done.wait();
}
protected:
virtual void start() { /* for derived types to implement */ }
virtual void stop() { /* for derived types to implement */ }
virtual void execute() { /* for derived types to implement */ }
std::mutex _mx;
std::thread _thread;
std::future< void > _done;
};
并使用我从中派生的以下应用程序示例来创建两个记录器对象,这些对象在特定时间范围内进行虚拟打印:
class logger : public basic_executor {
public:
logger() { /* ... */}
~logger() {
_mx.lock();
std::cout << "logger destructor " << std::endl;
_mx.unlock();
}
void execute() override {
std::this_thread::sleep_for(1s);
for (int i = 0; i < 10; ++i) {
_mx.lock();
printf("L1: I am printing something\n");
_mx.unlock();
std::this_thread::sleep_for(1s);
}
}
void stop() override {
_mx.lock();
printf("L1: I am done!\n");
_mx.unlock();
}
};
class logger2 : public basic_executor {
public:
logger2() { /* ... */}
~logger2() {
_mx.lock();
printf("logger2 destructor\n");
_mx.unlock();
}
void execute() override {
for (int i = 0; i < 10; ++i) {
_mx.lock();
printf("L2: I am ALSO printing something\n");
_mx.unlock();
std::this_thread::sleep_for(2s);
}
}
void stop() override {
_mx.lock();
printf("L2: I am done!\n");
_mx.unlock();
}
};
int main(int argc, char const *argv[]) {
/* code */
// printf("log:\n");
logger log1;
// printf("log1:\n");
logger2 log2;
printf("----------------------------------!\n");
log2.launch();
log1.launch();
// log1.wait();
// log2.wait();
printf("----------------------------------!\n");
return 0;
}
程序出现意外行为:
Ctor @0x7fff8b18c990
Ctor @0x7fff8b18c9e0
----------------------------------!
----------------------------------!
logger2 destructor
Dtor @0x7fff8b18c9e0
Joined thread @0x7fff8b18c9e0
logger destructor
Dtor @0x7fff8b18c990
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
L1: I am printing something
Joined thread @0x7fff8b18c990
因为 偶尔,'log2' 对象在被销毁之前从未开始执行,或者对其析构函数的 'join()' 调用无限期挂起。发生这种情况有什么明显的原因吗?我到底错过了什么?
错误可能发生在日志记录 class 中。但是,对于未定义的行为,您没有任何保证,也没有任何一致结果的期望。到目前为止,您只观察到两个日志记录 classes 之一的相同错误。虽然我可以解释为什么,但实际上,这并不重要。该错误可能发生在任何一个对象上。让我们从这里开始:
_thread = std::thread(std::move(task));
你不会得到任何保证,即新的执行线程将在这段代码继续之前立即开始执行以下任何内容,returns 来自 launch()
:
std::packaged_task< void() > task([this] {
start();
execute();
stop();
});
大多数时候,实际上,这将在新的执行线程中很快 运行 启动。但你不能依赖它。 C++ 向您保证的是,在某个时刻 在 std::thread
完成构建一个新的执行线程后将启动 运行。它可能是立竿见影的。或者,它可能会晚几百毫秒,因为您的操作系统有更重要的事情要做。
您期望新的执行线程将始终“立即”开始执行,同时 std::thread
正在构建。那不是真的。毕竟,您可能 运行 是一个 CPU 核心,并且在构建 std::thread
对象之后,您将继续在同一个执行线程中执行后续操作,并且仅在稍后发生上下文切换,到新的执行线程。
同时:
launch
() returns.父执行线程到达
main()
末尾。自动作用域中的所有对象都将被一一销毁。
在 C++ 中,当一个对象由一个 superclass 和一个 subclass 组成时,subclass 首先被销毁,然后是 superclass。这就是 C++ 的工作原理。
因此,
logger
/logger2
subclass 的析构函数立即被调用并销毁其对象(只是logger
/logger2
subclass).现在 superclass 的析构函数被调用,以销毁 superclass。
~basic_executor
开始做自己的事,耐心等待。现在,最后,那个新的执行线程,还记得那个吗?你猜怎么着:它终于开始 运行,并勇敢地尝试执行
start()
、execute()
和stop()
。或者它可能首先设法通过start()
,但尚未达到execute()
。但是由于实际的记录器 subclass 现在已经被销毁了,猜猜会发生什么?没有什么。它消失了。 subclass 没有了。它不再是。它加入了无形的合唱团。它渴望峡湾。是前潜艇class。没有logger::execute
或logger2::execute()
了。