在析构函数中调用 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 对象之后,您将继续在同一个执行线程中执行后续操作,并且仅在稍后发生上下文切换,到新的执行线程。

同时:

  1. launch() returns.

  2. 父执行线程到达main()末尾。

  3. 自动作用域中的所有对象都将被一一销毁。

  4. 在 C++ 中,当一个对象由一个 superclass 和一个 subclass 组成时,subclass 首先被销毁,然后是 superclass。这就是 C++ 的工作原理。

  5. 因此,logger/logger2 subclass 的析构函数立即被调用并销毁其对象(只是 logger /logger2 subclass).

  6. 现在 superclass 的析构函数被调用,以销毁 superclass。 ~basic_executor开始做自己的事,耐心等待。

  7. 现在,最后,那个新的执行线程,还记得那个吗?你猜怎么着:它终于开始 运行,并勇敢地尝试执行 start()execute()stop()。或者它可能首先设法通过 start(),但尚未达到 execute()。但是由于实际的记录器 subclass 现在已经被销毁了,猜猜会发生什么?没有什么。它消失了。 subclass 没有了。它不再是。它加入了无形的合唱团。它渴望峡湾。是前潜艇class。没有 logger::executelogger2::execute() 了。