这段代码安全吗,可以从构造函数 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
没有做你认为的事情:它声明 xfer
是 friend
的 runnable
(没用)而不是 runnable
是 @987654327 @ of xfer
(我猜这是你的意图)。
将 runnable 声明为 xfer 的朋友的目的是访问 xfer 的私有状态和 operator()() 函数的类型,以像 xfer 类的方法一样工作。没有办法将成员函数传递给线程。因此声明。
我已经声明了您建议的方式,它也具有相同的效果。谢谢
@RavikumarTulugu: runnnable
是xfer
的内部类,因此它已经可以访问内部。那里不需要友谊。
我试过这样做,但似乎只有在外部类的成员变量是静态的情况下才有效。
【参考方案1】:
一般来说,在构造函数中启动线程是没有问题的,
提供线程中使用的对象已经完全构造
在你开始之前。 (因此,例如,有一个线程的成语
基类,它在其构造函数中自行启动线程,是
坏了。)在你的情况下,你还没有达到这个标准,因为
线程对象使用您的成员run
,直到
在你开始线程之后。将线程的创建移入
构造函数的主体,或者只是改变数据的顺序
类定义中的成员将更正此问题。 (如果你这样做
后者,做添加注释,表明顺序很重要,
以及为什么。)
在析构函数中调用join
问题更大。任何操作
恕我直言,这可能会等待不确定的时间是有问题的
一个析构函数。在堆栈展开期间调用析构函数时,您
不想坐等其他线程完成。
此外,您可能希望使该类不可复制。 (在这种情况下,
你不需要shared_ptr
。)如果你复制,你最终会做
join
在同一个线程上两次。
【讨论】:
"在析构函数中调用join
问题更大。"不这样做会更有问题,因为销毁std::thread
即joinable()
会调用std::terminate()
[thread.thread.destr]...
@MarcMutz-mmutz 这可能更可取。在其他情况下,您可能想要通知线程它应该终止,然后是detach
。你不想做的是在析构函数中有一个不确定的等待。
我打算如何解决这个问题是在调用 join 之前停止析构函数中的线程(通过询问它)。这使得连接在很短的时间内返回。但是,正如您所提到的,它会稍微延迟堆栈展开过程。
@RavikumarTulugu 我也会将其分离。即使您请求终止它,它也无法终止,直到它被安排好,而且您不知道那会是什么时候。
@JamesKanze 然后你会留下一个指向 xfer 的悬空指针,这将调用 UB。怎么样?【参考方案2】:
您的代码存在竞态条件并且不安全。
问题是成员变量的初始化是按照它们在类中声明的顺序发生的,这意味着成员thr
在成员run
之前被初始化。在thr
的初始化期间,您在run
上调用std::bind
,这将在内部复制(尚未初始化的)run
对象(您知道您正在那里复制吗?)。
假设您传递了一个指向 run
的指针(或使用 std::ref
到 std::bind
以避免复制),代码仍然会有竞争条件。在这种情况下,将指针/引用传递给std::bind
不会成为问题,因为可以将指针/引用传递给尚未初始化的成员,只要它未被访问。 但是,仍然存在竞争条件,即std::thread
对象可能会太快地生成线程(比如运行构造函数的线程被驱逐,并且新线程处理),并且可以结束随着线程执行run.operator()
之前 run
对象已被初始化。
您需要重新排序类型中的字段以确保代码安全。既然您知道成员顺序的微小变化会对代码的有效性产生干扰影响,您还可以考虑更安全 的设计。 (此时它是正确的,它可能实际上是您需要的,但至少注释类定义,以便以后没有人重新排序字段)
【讨论】:
@RavikumarTulugu:请注意 James 指出的问题:析构函数不应花费不确定的时间来完成(如果抛出异常,请考虑堆栈展开),并且您需要使类型不可复制(考虑将std::thread
直接存储在类中,而不是通过shared_ptr
,否则容易出现细微错误:xfer
对象的副本将导致对单个join()
对象的多个join()
调用这可能会使您的整个应用程序崩溃)。也考虑接受他的回答。【参考方案3】:
在构造函数中使用可能是安全的,但是......
你应该避免在函数调用中使用 barenew
在析构函数中使用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
类中。
根据经验,一个对其元素子集有特殊复制/分配需求的类可能应该被拆分,以将具有特殊需求的元素隔离在一个(或多个)专用类中。 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++ 生成线程吗?的主要内容,如果未能解决你的问题,请参考以下文章