PyQt пробуждает основной поток из не-QThread

#python #multithreading #qt #pyqt #qthread

#python #многопоточность #qt #pyqt #qthread

Вопрос:

У меня есть приложение PyQt, которое получает информацию из внешнего источника с помощью обратных вызовов, которые вызываются из потоков, которые не находятся под моим контролем, а которые нет QThread . Каков правильный способ передачи такой информации в основной поток без опроса? В частности, я хочу выдать сигнал Qt, чтобы я мог разбудить основной поток (или другой QThread ) при поступлении новых данных.

Комментарии:

1. Не уверен, что понимаю, разве вы не можете изменить исходный код потока, отличного от qt? как испускать сигнал по завершении?

2. Единственное, что находится под моим контролем, — это сама функция обратного вызова, которая является обычной функцией python. Т.Е. я мог бы излучать сигналы, но будет ли это разрешено? Поток, который будет запускать эту функцию, не создан моим кодом и вообще не имеет понятия о Qt. Так что, может быть, мне следует спросить, является ли передача сигналов потокобезопасной (даже из потоков, отличных от Qt)?

3. Я не знаю, является ли это лучшей практикой или плохой, но именно так я бы попытался сделать, отправить сигнал от вашего обратного вызова. Я думаю, это должно сработать. ( Безопасность потоков Qt зависит от типа соединения: doc.qt.io/qt-4.8 /…

4. В документах, которые вы связываете, указано: «С другой стороны, вы можете безопасно передавать сигналы из своей реализации QThread::run(), поскольку излучение сигнала является потокобезопасным». Во второй части говорится, что ваше предложение в порядке, но первая часть, похоже, снова ограничивает это утверждение QThreads…

5. Безопасность потоков означает, что он должен работать, даже если он не находится в QThread потоке на основе a.

Ответ №1:

Тип соединения по умолчанию для сигналов — Qt.AutoConnection, который в документах описывается следующим образом:

Если сигнал передается из другого потока, отличного от принимающего объекта, сигнал помещается в очередь и ведет себя как Qt::QueuedConnection. В противном случае слот вызывается напрямую, действуя как Qt::DirectConnection. Тип соединения определяется при отправке сигнала.

Поэтому перед отправкой сигнала Qt просто сравнивает текущую привязку потока отправителя и получателя, прежде чем решить, ставить его в очередь или нет. Не имеет значения, как изначально были запущены базовые потоки.

Вот простая демонстрация с использованием рабочего потока python:

 import sys, time, threading
from PyQt4 import QtCore, QtGui

class Worker(object):
    def __init__(self, callback):
        self._callback = callback
        self._thread = None

    def active(self):
        return self._thread is not None and self._thread.is_alive()

    def start(self):
        self._thread = threading.Thread(target=self.work, name='Worker')
        self._thread.start()

    def work(self):
        print('work: [%s]' % threading.current_thread().name)
        for x in range(5):
            time.sleep(1)
            self._callback(str(x))

class Window(QtGui.QPushButton):
    dataReceived = QtCore.pyqtSignal(str)

    def __init__(self):
        super(Window, self).__init__('Start')
        self.clicked.connect(self.start)
        self.dataReceived.connect(self.receive)
        self.worker = Worker(self.callback)

    def receive(self, data):
        print('received: %s [%s]' % (data, threading.current_thread().name))

    def callback(self, data):
        print('callback: %s [%s]' % (data, threading.current_thread().name))
        self.dataReceived.emit(data)

    def start(self):
        if self.worker.active():
            print('still active...')
        else:
            print('start: [%s]' % threading.current_thread().name)
            self.worker.start()

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    print('show: [%s]' % threading.current_thread().name)
    sys.exit(app.exec_())
  

Типичный вывод:

 $ python test.py
show: [MainThread]
start: [MainThread]
work: [Worker]
callback: 0 [Worker]
received: 0 [MainThread]
still active...
callback: 1 [Worker]
received: 1 [MainThread]
still active...
callback: 2 [Worker]
received: 2 [MainThread]
still active...
callback: 3 [Worker]
received: 3 [MainThread]
callback: 4 [Worker]
received: 4 [MainThread]
  

Комментарии:

1. Вы говорите, что Qt «сравнил текущую привязку потока отправителя и получателя»: как определяется привязка потока для отправителя? Это фактический поток (например threading.current_thread() ) или это поток, в котором находится отправляющий QObject (который в вашем примере определяется при Worker создании, что было бы неправильным потоком)? Первое имело бы больше смысла и сделало бы ваше решение правильным, в то время как последнее соответствует определению Qt doc «сходства потоков»: doc.qt.io/qt-5/qobject.html#thread-affinity

2. Извините, моя ошибка, отправляющий объект в вашем примере на самом деле такой же, как и принимающий объект, это Window (как сигнал dataReceived , так и слот callback являются частью Window ). Таким образом, если бы он действительно использовал «привязку к потоку» в соответствии с документами, он (ошибочно) выполнил бы прямой вызов. Поэтому я делаю вывод, что Qt действительно сравнивает текущий запущенный поток (вместо привязки потока отправителя) к привязке потока получателя.

3. @burnpanck. Да, «отправитель» означает рабочего в моем примере, потому что это то, что на самом деле вызывает функцию обратного вызова (на которую она ссылается).

Ответ №2:

Я бы справился с этим так же, как если бы вы просто опрашивали внешнее устройство или библиотеку. Создайте отдельный рабочий поток, который обрабатывает обратный вызов и отправляет сигнал в основной поток GUI.

 class Worker(QObject):

    data_ready = pyqtSignal(object, object)

    def callback(self, *args, **kwargs):
        self.data_ready.emit(args, kwargs)


class Window(...)

    def __init__(self):
        ...
        self.worker = Worker(self)
        # Connect worker callback function to your external library
        library.register_callback(self.worker.callback) #??
        ...

        # Mover worker to thread and connect signal
        self.thread = QThread(self)
        self.worker.data_ready.connect(self.handle_data)
        self.worker.moveToThread(self.thread)
        self.thread.start()

    @pyqtSlot(object, object)
    def handle_data(self, args, kwargs):
        # do something with data
  

Комментарии:

1. Действительно ли поток self.thread нужен? Что он будет делать? Функция Worker.callback , конечно, все равно будет выполняться в этом неизвестном чужом потоке, созданном library