这段代码安全吗,是否可以从构造函数C++生成线程

is this code safe , is it ok to spawn a thread from a constructor C++?

本文关键字:构造函数 C++ 线程 是否 段代码 代码 安全      更新时间:2023-10-16

我需要在C++类中嵌入一个线程,这是一种活动对象,但不完全是。 我正在从类的构造函数中生成线程,可以这样做吗 这种方法有任何问题吗?

#include <iostream>
#include <thread>
#include <unistd.h>
class xfer
{
        int i;
        std::shared_ptr<std::thread> thr;
        struct runnable {
                friend class xfer;
                void operator()(xfer *x) {
                        std::cerr<<"thread started, xfer.i:"<<x->i;
                }
        } run;
        public:
        xfer() try : i(100), thr(new std::thread(std::bind(run, this))) { } catch(...) { }
        ~xfer() { thr->join(); }
};
int
main(int ac, char **av)
{
        xfer x1;
        return 0;
}

通常,在构造函数中启动线程没有问题,前提是线程中使用的对象已完全构造在开始之前。 (因此,例如,具有线程的成语基类,在其构造函数中启动线程本身,是坏了。 在你的情况下,你没有遇到这个标准,因为线程对象使用您的成员run,直到启动线程后。 将线程的创建移动到构造函数的主体,或者只是更改数据的顺序类定义中的成员将更正此问题。 (如果您执行后者,添加注释,大意是顺序很重要,以及为什么。

在析构函数中调用join问题更多。 任何操作恕我直言,这可能会等待不确定的时间是有问题的析构函数。 在堆栈展开期间调用析构函数时,您不想坐在那里等待另一个线程完成。

此外,您可能希望使类不可复制。 (在这种情况下,你不需要shared_ptr。 如果你复制,你最终会做 在同一线程上join两次。

您的代码具有争用条件,不安全。

问题是成员变量的初始化按照它们在类中声明的顺序进行,这意味着成员thr在成员run之前初始化。在初始化thr期间,您将在run上调用std::bind,这将在内部复制(尚未初始化的)run对象(您知道您正在那里复制吗?

假设您传递了一个指向run的指针(或用于std::ref std::bind以避免复制),代码仍将具有争用条件。在这种情况下,将指针/引用传递给std::bind不会成为问题,因为只要不访问指针/引用,就可以将指针/引用传递给尚未初始化的成员。但是,仍然存在争用条件,即 std::thread 对象可能生成线程的速度太快(例如,运行构造函数的线程被逐出,新线程处理),并且最终可能导致线程在初始化run对象之前执行run.operator()

您需要对类型中的字段重新排序以使代码安全。既然您知道成员顺序的微小更改可能会对代码的有效性产生干扰影响,那么您也可以考虑更安全的设计。(在这一点上,这将是正确的,它实际上可能是你需要的,但至少注释类定义,以便以后没有人对字段重新排序)

在构造函数中使用可能是安全的,但是...

  • 应避免在函数调用中使用new
  • 在析构函数中使用thr->join()是...指示问题

在细节...

不要在函数调用中使用裸new

由于未指定操作数的执行顺序,因此它不是异常安全的。更喜欢在这里使用 std::make_shared,或者在 unique_ptr 的情况下,创建您自己的make_unique工具(使用完美的转发和可变参数模板,它就可以工作);这些构建器方法将确保 RAII 类的所有权分配和归属不可分割地执行。

知道三法则

三法则是经验法则(为 C++03 建立),它说如果你需要编写一个复制构造函数、赋值运算符或析构函数,你可能也应该写另外两个。

在您的情况下,生成的赋值运算符不正确。也就是说,如果我创建了两个对象:

 int main() {
     xfer first;  // launches T1
     xfer second; // launches T2
     first = second;
 }

然后在执行作业时,我丢失了T1的引用,但我从未加入过它!!

我不记得join调用是否是强制性的,但是您的类根本不一致。如果不是必需的,请删除析构函数;如果它是必需的,那么你应该编写一个只处理一个共享线程的较低级别的类,然后确保共享线程的销毁始终在join调用之前,最后直接在xfer类中使用该共享线程工具。

根据经验,可能应该拆分对其元素子集具有特殊复制/分配需求的类,以将具有特殊需求的元素隔离在一个(或几个)专用类中。