考虑下面的代码,我声明了一个简单的类来执行异步/线程操作:
#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 @%pn", this);
_mx.unlock();
}
virtual ~basic_executor() {
_mx.lock();
printf("Dtor @%pn", this);
_mx.unlock();
if (_thread.joinable()) {
_thread.join();
_mx.lock();
printf("Joined thread @%pn", 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 somethingn");
_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 destructorn");
_mx.unlock();
}
void execute() override {
for (int i = 0; i < 10; ++i) {
_mx.lock();
printf("L2: I am ALSO printing somethingn");
_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()'调用无限期挂起。发生这种情况有什么明显的原因吗,我到底错过了什么?
任何一个日志类都可能出现该错误。然而,对于未定义的行为,您没有任何保证,也没有任何类型的一致结果的期望。到目前为止,您只在两个日志记录类中的一个中观察到相同的错误。虽然我可以解释为什么,但实际上,这无关紧要。这两个对象中的任何一个都可能发生错误。我们从这里开始:
_thread = std::thread(std::move(task));
在此代码继续执行并从launch()
返回之前,您将无法获得任何保证新的执行线程将立即开始执行以下任何操作:
std::packaged_task< void() > task([this] {
start();
execute();
stop();
});
大多数时候,实际上,这将在新的执行线程中很快开始运行。但你不能依赖它。c++所保证的只是在之后的某个点std::thread
完成构建一个新的执行线程将开始运行。它可能是即时的。或者,它可能会延迟几百毫秒,因为您的操作系统有更重要的事情要做。
您期望新的执行线程总是"立即"开始执行,同时构建std::thread
。这不是真的。毕竟,您可能正在使用单个CPU核心运行,并且在构造std::thread
对象之后,您将继续在相同的执行线程中执行下面的内容,并且仅在稍后发生上下文切换到新的执行线程。
与此同时:
launch
()返回。父执行线程到达
main()
结束自动作用域中的所有对象都将被一个接一个地销毁。
在c++中,当一个对象由超类和子类组成时,首先销毁子类,然后销毁超类。这就是c++的工作原理。
所以,
logger
/logger2
子类的析构函数被立即调用,它破坏了它的对象(只是logger
/logger2
子类)。现在父类的析构函数被调用,以销毁父类。
~basic_executor
开始做它的事情,耐心地等待。现在,最后,新的执行线程,还记得那个吗?猜猜结果如何:它终于开始运行,并勇敢地尝试执行
start()
、execute()
和stop()
。或者它可能首先成功地通过了start()
,但还没有到达execute()
。但是由于实际的记录器子类现在已经被销毁了,猜猜会发生什么?什么都没有。这是一去不复返了。子类不再存在。它不再是。它加入了无形的唱诗班。它渴望着峡湾。它是一个前子类。没有logger::execute
或logger2::execute()
了。