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

Posted

技术标签:

【中文标题】这段代码安全吗,可以从构造函数 C++ 生成线程吗?【英文标题】:is this code safe , is it ok to spawn a thread from a constructor C++? 【发布时间】:2012-06-14 11:27:03 【问题描述】:

我需要在 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;

【问题讨论】:

friend class xfer 没有做你认为的事情:它声明 xferfriendrunnable (没用)而不是 runnable 是 @987654327 @ of xfer(我猜这是你的意图)。 将 runnable 声明为 xfer 的朋友的目的是访问 xfer 的私有状态和 operator()() 函数的类型,以像 xfer 类的方法一样工作。没有办法将成员函数传递给线程。因此声明。 我已经声明了您建议的方式,它也具有相同的效果。谢谢 @RavikumarTulugu: runnnablexfer 的内部类,因此它已经可以访问内部。那里不需要友谊。 我试过这样做,但似乎只有在外部类的成员变量是静态的情况下才有效。 【参考方案1】:

一般来说,在构造函数中启动线程是没有问题的, 提供线程中使用的对象已经完全构造 在你开始之前。 (因此,例如,有一个线程的成语 基类,它在其构造函数中自行启动线程,是 坏了。)在你的情况下,你还没有达到这个标准,因为 线程对象使用您的成员run,直到 在你开始线程之后。将线程的创建移入 构造函数的主体,或者只是改变数据的顺序 类定义中的成员将更正此问题。 (如果你这样做 后者,添加注释,表明顺序很重要, 以及为什么。)

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

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

【讨论】:

"在析构函数中调用join 问题更大。"不这样做会更有问题,因为销毁std::threadjoinable() 会调用std::terminate() [thread.thread.destr]... @MarcMutz-mmutz 这可能更可取。在其他情况下,您可能想要通知线程它应该终止,然后是detach。你不想做的是在析构函数中有一个不确定的等待。 我打算如何解决这个问题是在调用 join 之前停止析构函数中的线程(通过询问它)。这使得连接在很短的时间内返回。但是,正如您所提到的,它会稍微延迟堆栈展开过程。 @RavikumarTulugu 我也会将其分离。即使您请求终止它,它也无法终止,直到它被安排好,而且您不知道那会是什么时候。 @JamesKanze 然后你会留下一个指向 xfer 的悬空指针,这将调用 UB。怎么样?【参考方案2】:

您的代码存在竞态条件并且不安全。

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

假设您传递了一个指向 run 的指针(或使用 std::refstd::bind 以避免复制),代码仍然会有竞争条件。在这种情况下,将指针/引用传递给std::bind 不会成为问题,因为可以将指针/引用传递给尚未初始化的成员,只要它未被访问但是,仍然存在竞争条件,即std::thread 对象可能会太快地生成线程(比如运行构造函数的线程被驱逐,并且新线程处理),并且可以结束随着线程执行run.operator() 之前 run 对象已被初始化。

您需要重新排序类型中的字段以确保代码安全。既然您知道成员顺序的微小变化会对代码的有效性产生干扰影响,您还可以考虑更安全 的设计。 (此时它是正确的,它可能实际上是您需要的,但至少注释类定义,以便以后没有人重新排序字段)

【讨论】:

@RavikumarTulugu:请注意 James 指出的问题:析构函数不应花费不确定的时间来完成(如果抛出异常,请考虑堆栈展开),并且您需要使类型不可复制(考虑将std::thread 直接存储在类中,而不是通过shared_ptr,否则容易出现细微错误:xfer 对象的副本将导致对单个join() 对象的多个join() 调用这可能会使您的整个应用程序崩溃)。也考虑接受他的回答。【参考方案3】:

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

你应该避免在函数调用中使用 bare new 在析构函数中使用thr-&gt;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 类中。

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

【讨论】:

在构造函数中的使用可能是安全的,但是...... 但事实并非如此。这里的初始化顺序破坏了代码的正确性。 std::bind 将从尚未初始化的run 成员复制...否则,此答案是用户(和其他人)遵循的合理建议! (虽然我在这里不太看到shared_ptr 的问题) @DavidRodríguez-dribeas:确切地说,我引用了一个事实,即操作顺序未指定但忘记检查它是否影响new之外的任何其他内容。 new here 没有问题,因为没有其他操作可能会抛出,但一般来说,如果在初始化列表中执行的另一个操作可能会抛出,那么对 new 的调用将是不安全的因为异常可能发生在new 的结尾和shared_ptr 的构造之间。 初始化中的操作顺序是明确定义的。另外,由于内存是直接在shared_ptr 中管理的,如果new 成功,那么shared_ptr 将获得内存的所有权,如果以后出现异常,将释放它。最后,代码中根本不需要指针,std::thread 很可能是对象的成员并直接初始化......除非设计要求xfer 的多个副本共享单个的所有权线程... 要完成@DavidRodríguez-dribeas 所说的:每次初始化之间都有一个序列点,因此必须先完成一次初始化的所有副作用,然后编译器才能开始下一次初始化。使用new 在初始化列表中初始化智能指针总是安全的。 @MatthieuM。我在写上面的时候正在考虑它,但我不想让评论变得比它更复杂,并假设它是一个空类型,仅用于说明。现在,实际上,根据标准,代码实际上是表现良好的。对于具有普通构造函数的类型,对象生命周期在分配内存后立即开始,因此在此问题中,run 在线程创建之前 已经存在,并且代码是正确的。话虽如此,我担心声明代码正确会被解读为 blank 声明。

以上是关于这段代码安全吗,可以从构造函数 C++ 生成线程吗?的主要内容,如果未能解决你的问题,请参考以下文章

c++类的构造函数不显式声明会自动生成吗

C++ 无法理解为啥这段代码不是线程安全的

Files.copy 是 Java 中的线程安全函数吗?

c++中用*this返回一个对象,会调用复制构造函数吗?

“空”构造函数或析构函数会与生成的构造函数做同样的事情吗?

从 C++ 中的多个线程调用 Qt 中小部件类的信号函数是不是安全?