Qt5.3 的 QObject::moveToThread(( 文档解释说,如果对象有父对象,moveToThread()
方法可能会失败。如何在代码中检测此故障?
我意识到,简单地确保我的对象没有父对象可能就足够了,但作为一种防御性编程实践,我想测试所有可能失败的调用的返回值。
编辑:我想在一些答案之后强调,我完全知道我可以在调用 moveToThread 之前测试父是否为 0。我正在寻找可能的方法,根据经验确定moveToThread
调用实际上是否成功。
为了可靠地获取moveToThread()
的结果,捕获正在移动的对象ThreadChange
事件(通过覆盖QObject::event()
或安装事件过滤器(,并存储该事件是否已在对局部变量的引用中看到:
static bool moveObjectToThread(QObject *o, QThread *t) {
class EventFilter : public QObject {
bool &result;
public:
explicit EventFilter(bool &result, QObject *parent = nullptr)
: QObject(parent), result(result) {}
bool eventFilter(QObject *, QEvent *e) override {
if (e->type() == QEvent::ThreadChange)
result = true;
return false;
}
};
bool result = false;
if (o) {
o->installEventFilter(new EventFilter(result, o));
o->moveToThread(t);
}
return result;
}
说来话长:
文档错误。您可以将具有父线程的
QObject
移动到另一个线程。为此,您只需在要移动的QObject
层次结构的根上调用moveToThread()
,所有子级也将移动(这是为了确保父母和他们的孩子始终在同一线程上(。我知道,这是一个学术上的区别。只是在这里彻底。当
QObject
的thread()
未== QThread::currentThread()
时,moveToThread()
调用也可能失败(即,您只能将对象推送到另一个线程,但不能从另一个线程中提取一个对象(。最后一句是对孩子的谎言。如果对象之前已与任何线程解除关联,则可以拉取该对象(通过调用
moveToThread(nullptr)
。当线程相关性更改时,将向对象发送
QEvent::ThreadChange
事件。
现在,您的问题是如何可靠地检测到移动发生。答案是:这并不容易。显而易见的第一件事,将moveToThread()
调用后的QObject::thread()
返回值与moveToThread()
参数进行比较不是一个好主意,因为QObject::thread()
不是(记录为(线程安全的(参见实现(。
为什么这是一个问题?
一旦moveToThread()
返回,移动到的线程可能已经开始执行"对象",即该对象的事件。作为该处理的一部分,可能会删除该对象。在这种情况下,对原始线程上的以下QObject::thread()
调用将取消引用已删除的数据。或者,新线程会将对象交给另一个线程,在这种情况下,在原始线程中调用thread()
的成员变量的读取将与在新线程中moveToThread()
内对同一成员变量的写入竞争。
底线:从原始线程访问 moveToThread()
ed 对象是未定义的行为。别这样。
唯一的方法是使用 ThreadChange
事件。该事件是在检查完所有失败案例后发送的,但至关重要的是,仍然来自原始线程(参见实现;如果实际上没有发生线程更改,发送这样的事件也是完全错误的(。
可以通过对移动到的对象进行子类化并重新实现QObject::event()
或通过在要移动的对象上安装事件过滤器来检查事件。
当然,事件过滤器方法更好,因为您可以将其用于任何QObject
,而不仅仅是那些您可以或想要子类的。但是,有一个问题:一旦发送了事件,事件处理就会切换到新线程,因此事件过滤器对象将从两个线程中锤击,这绝不是一个好主意。简单的解决方案:使事件过滤器成为要移动的对象的子级,然后它将随之移动。另一方面,这给您带来了如何控制存储的生存期的问题,这样即使移动的对象在到达新线程时立即被删除,您也可以获得结果。长话短说:存储需要是对旧线程中变量的引用,而不是要移动的对象或事件过滤器的成员变量。然后,对存储的所有访问都来自原始线程,并且没有争用。
,但是...那不是还不安全吗?是的,但前提是对象再次移动到另一个线程。在这种情况下,事件筛选器将从第一个移动到的线程访问存储位置,这将与来自原始线程的读取访问竞争。简单的解决方案:在触发一次事件过滤器后将其卸载。该实现留给读者:)
QObject::moveToThread
只有在具有父级时才失败。如果它的父级是NULL
那么你可以移动它,否则你不能。
编辑:
你可以做的是,在调用moveToThread
后,你可以通过调用 QObject::thread 并检查它是否真的改变了它的亲和力来检查对象的线程亲和力。
QThread *pThread = new QThread;
QObject *pObject = new QObject;
{
QMutexLocker locker(&mutex);
pObject->moveToThread(pThread);
if(pObject->thread() != pThread)
{
qDebug() << "moveToThread failed.";
}
}