c++multithreadingqtqwidget

How to access QWidget from other threads


I have

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

I need to access a MyWidget instance from another thread (i.e. not the main thread). Is there any way to do that safely? I understand that I cannot access GUI related functions because some backends (e.g. MacOSX/Cocoa) don't support that. However, I only need to access data or doSth() in this example. But from what I have understand, there is simply no way to guarantee the lifetime of the object - i.e. if the parent window with that widget closes, the MyWidget instance gets deleted.

Or is there a way to guarantee the lifetime? I guess QSharedPointer doesn't work because the QWidget does its lifetime handling internally, depending on the parent widget. QPointer of course also doesn't help because it is only weak and there is no locking mechanism.

My current workaround is basically:

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

(execInMainThread_sync works by using QMetaMethod::invoke to call a method in the main thread.)

However, that workaround doesn't work anymore for some specific reason (I will explain later why, but that doesn't matter here). Basically, I am not able to execute something in the main thread at that point (for some complicated deadlock reasons).

Another workaround I'm currently thinking about is to add a global mutex which will guard the MyWidget destructor, and in the destructor, I'm cleaning up other weak references to the MyWidget. Then, elsewhere, when I need to ensure the lifetime, I just lock that mutex.


The reason why my current workaround doesn't work anymore (and that is still a simplified version of the real situation):

Note: The Python global interpreter lock is not really the problem. Of course it gets unlocked before any execInMainThread_sync call. However, I cannot really check for any other potential Python/whatever locks. Esp. I am not allowed to just unlock the Python-import-lock -- it's there for a reason.

One solution you might think of is to really just avoid any Python code at all in the main thread. But that has a lot of drawbacks, e.g. it will be slow, complicated and ugly (the GUI basically only shows data from Python, so there need to be a huge proxy/wrapper around it all). And I think I still need to wait at some points for the Python data, so I just introduce the possible deadlock-situation at some other point.

Also, all the problems would just go away if I could access MyWidget safely from another thread. Introducing a global mutex is the much cleaner and shorter solution, compared to above.


Solution

  • My solution:

    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));
    }
    

    Now, everywhere I need to pass around a MyWidget pointer, I'm using:

    MyWidget::WeakRef widget;
    

    And I can use it from another thread like this:

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

    This is safe. As long as ScopedRef exists, MyWidget cannot be deleted. It will block in its destructor. Or it is already deleted and ScopedRef::ptr == NULL.