如何从其他线程访问QWidget

How to access QWidget from other threads

本文关键字:访问 QWidget 线程 其他      更新时间:2023-10-16

我有

struct MyWidget : QWidget {
// non-GUI related stuff:
int data;
int doSth();
};

我需要从另一个线程(即不是主线程)访问MyWidget实例。有什么安全的方法吗?我知道我无法访问与GUI相关的功能,因为有些后端(例如MacOSX/Cocoa)不支持。但是,在本例中,我只需要访问datadoSth()。但据我所知,根本无法保证对象的生存期——也就是说,如果包含该小部件的父窗口关闭,MyWidget实例就会被删除。

或者有没有办法保证你的一生?我想QSharedPointer不起作用,因为QWidget在内部进行其生存期处理,这取决于父窗口小部件。QPointer当然也没有帮助,因为它只是弱的,并且没有锁定机制。

我目前的解决方法基本上是:

int widget_doSth(QPointer<MyWidget> w) {
int ret = -1;
execInMainThread_sync([&]() {
if(w)
ret = w->doSth();
});
return ret;
}

(execInMainThread_sync通过使用QMetaMethod::invoke调用主线程中的方法来工作。)

然而,由于某些特定的原因,这种变通方法不再有效(我稍后会解释原因,但这在这里无关紧要)。基本上,我无法在主线程中执行某些内容(由于一些复杂的死锁原因)。

我目前正在考虑的另一个解决方法是添加一个全局互斥,它将保护MyWidget析构函数,在析构函数中,我正在清理对MyWidget的其他弱引用。然后,在其他地方,当我需要确保生存期时,我只需锁定互斥锁。


我当前的解决方案不再有效的原因(这仍然是真实情况的简化版本):

  • MyWidget中,data实际上是PyObject*

  • 在主线程中,会调用一些Python代码。(在我的应用程序的主线程中,根本不可能避免任何Python代码调用。)该Python代码最终执行了一些import,它由一些Python导入互斥体保护(Python不允许并行imports)

  • 在其他一些Python线程中,会调用其他一些importimport现在锁定了Python导入互斥体。当它在做自己的事情时,它会在某个时候进行一些GC清理。GC清理调用某个对象的遍历函数,该对象包含MyWidget。因此,它必须访问MyWidget。但是,execInMainThread_sync(或等效的工作解决方案)将死锁,因为主线程当前正在等待Python导入锁。

注意:Python全局解释器锁并不是真正的问题。当然,它会在任何execInMainThread_sync调用之前解锁。然而,我无法真正检查任何其他潜在的Python/whatever锁。特别是,我不允许只解锁Python导入锁——这是有原因的。

你可能会想到的一个解决方案是真正避免在主线程中使用任何Python代码。但这有很多缺点,例如它会很慢、复杂和丑陋(GUI基本上只显示来自Python的数据,因此需要有一个巨大的代理/包装器)。我认为在某些时候我仍然需要等待Python数据,所以我只在其他时候介绍可能的死锁情况。

此外,如果我可以从另一个线程安全地访问MyWidget,那么所有的问题都会消失。与上面的解决方案相比,引入全局互斥是更干净、更短的解决方案。

您可以使用信号/插槽机制,但如果GUI控件的数量很大,它可能会很乏味。我建议使用单个信号和插槽来控制gui。通过struct发送更新GUI所需的所有信息。

void SomeWidget::updateGUISlot(struct Info const& info)
{
firstControl->setText(info.text);
secondControl->setValue(info.value);
}

如果收件人被删除,您不需要担心发出信号。此细节由Qt处理。或者,您可以在退出GUI线程事件循环后等待线程退出。您需要使用Qt注册该结构。

编辑:

根据我从你的扩展问题中读到的内容,你的问题与线程之间的通信有关。尝试管道、(POSIX)消息队列、套接字或POSIX信号,而不是用于线程间通信的Qt信号。

就我个人而言,我不喜欢GUI内容(即:小部件)具有非GUI相关内容的设计。。。我认为你应该把这两者分开。Qt需要将GUI对象始终保持在主线程上,但任何其他对象(QObject派生的)都可以移动到线程(QObject::moveToThread)。

您所解释的似乎与小部件、Qt或类似的东西完全无关。这是Python及其线程和锁结构固有的问题,如果你是多线程的,这是没有意义的。Python基本上假设任何对象都可以从任何线程访问。使用任何其他工具包都会遇到同样的问题。可能有一种方法可以告诉Python不要这样做——我对cpython实现的细节了解不够,但这正是你需要了解的地方。

GC清理调用某个对象的遍历函数,该对象包含MyWidget

这是你的问题。您必须确保这种跨线程GC清理不会发生。我不知道你会怎么做:(

我担心的是,尽管每个人都声称只有C/C++才能让你在如此大规模的规模上做到这一点,但你使用Python却悄悄地、巧妙地给自己开了一枪。

我的解决方案:

struct MyWidget : QWidget {
// some non-GUI related stuff:
int someData;
virtual void doSth();   
// We reset that in the destructor. When you hold its mutex-lock,
// the ref is either NULL or a valid pointer to this MyWidget.
struct LockedRef {
boost::mutex mutex;
MyWidget* ptr;      
LockedRef(MyWidget& w) : ptr(&w) {}
void reset() {
boost::mutex::scoped_lock lock(mutex);
ptr = NULL;
}
};  
boost::shared_ptr<LockedRef> selfRef;
struct WeakRef;
struct ScopedRef {
boost::shared_ptr<LockedRef> _ref;
MyWidget* ptr;
bool lock;
ScopedRef(WeakRef& ref);
~ScopedRef();
operator bool() { return ptr; }
MyWidget* operator->() { return ptr; }
};
struct WeakRef {
typedef boost::weak_ptr<LockedRef> Ref;
Ref ref;
WeakRef() {}
WeakRef(MyWidget& w) { ref = w.selfRef; }
ScopedRef scoped() { return ScopedRef(*this); }
};
MyWidget();
~MyWidget();
};

MyWidget::ScopedRef::ScopedRef(WeakRef& ref) : ptr(NULL), lock(true) {
_ref = ref.ref.lock();
if(_ref) {
lock = (QThread::currentThread() == qApp->thread());
if(lock) _ref->mutex.lock();
ptr = _ref->ptr;
}
}
MyWidget::ScopedRef::~ScopedRef() {
if(_ref && lock)
_ref->mutex.unlock();
}
MyWidget::~QtBaseWidget() {
selfRef->reset();
selfRef.reset();
}
MyWidget::MyWidget() {
selfRef = boost::shared_ptr<LockedRef>(new LockedRef(*this));
}

现在,在我需要绕过MyWidget指针的任何地方,我都使用:

MyWidget::WeakRef widget;

我可以从另一个线程这样使用它:

MyWidget::ScopedRef widgetRef(widget);
if(widgetRef)
widgetRef->doSth();

这是安全的。只要ScopedRef存在,就不能删除MyWidget。它将在其析构函数中阻塞。或者它已经被删除并且ScopedRef::ptr == NULL