Очередь доступа в QThread

Задача простая - создаем сцену, на нее добавляем объекты (~400к+шт).
Объекты должны изменять свои параметры в режиме реального времени, но делать расчеты новых параметров в основном потоке никак нельзя, так как сцена будет тормозить.
Расчеты нужно производить в другом потоке (желательно каждые 100мс) и там же обновлять параметры объектов, а основной поток сцены должен продолжать рисовать объекты.

Код:

class CalcThread(QThread):

    def __init__(self, parent=None, scene: QGraphicsScene = None):
        QThread.__init__(self, parent)

        self.scene = scene

    def run(self):

        while True:
           
            #  Долгие расчеты новых параметров для объектов сцены..
            self.scene.update(self.scene.views()[0].mapToScene(self.scene.views()[0].rect()).boundingRect())
            print(self.scene.views()[0].rect(), len(self.scene.items()))
            time.sleep(1)


class Widget_GraphSurface(QWidget):

    def __init__(self):
        super().__init__()

        self.mainScene = QGraphicsScene()
        self.mainScene.addItem(добавляем много объектов....)
        self.ggv_main = QGraphicsView(self.mainScene)

        mThread = QThread()
        cThread = CalcThread(None, self.mainScene)
        cThread.moveToThread(mThread)
        cThread.start()

Если запустить в таком виде, вроде бы в методе run, потока CalcThread, все работает как нужно, а именно - он блокирует доступ к mainScene основному потоку, пока проводит расчеты. Но сцена начинает сильно тормозить, вроде все логично, основной поток не может получить self.mainscene пока она занята в CalcThread. Я так полагаю, что управление self.mainscene, возвращается после каждой завершенной итерации метода run из CalcThread. Задача все же состоит в том, чтобы сделать незаметными расчеты, чтобы сцена продолжала плавно рисовать элементы. Были предприняты попытки:

  1. Делать расчеты в обычном потоке threading.Thread - все летало, до того момента, как поток из threading.Thread и MainThread(основной поток сцены), пытались получить доступ к сцене одновременно - получаем аксес виолайшн
  2. Метод 1 + использование Lock, RLock, семафоры и т.д. - итог один аксес виолейшн
  3. Передача в поток CalcThread напрямую всего экземпляра класса со сценой и вызов из него функции расчетов - итог - аксес виолешн
  4. Метод 3 + кьютишные мютексы и локи - аксес виолейшн

Итого: лучше всего на данный момент работает, если расчеты делать в том же потоке, где и сцена, но картинка подтормаживает, а хочется, наоборот. Возникает несколько вопросов:

  1. Я не правильно использую QThread, что нужно исправить?
  2. Может нужно использовать кьютишные мютексы в потоке CalcThread?
  3. Может нужно организовать очередь доступа к объекту сцены?
  4. Нужен другой подход к этому вопросу?

По сути мне нужно:

  1. В потоке CalcThread получить сцену
  2. Получить некоторые объекты со сцены
  3. Получить параметры сцены и этих объектов
  4. Отдать управление сценой обратно в MainThread
  5. Произвести расчеты
  6. Снова получить сцену в потоке CalcThread
  7. Изменить параметры объектов на сцене
  8. Отдать сцену обратно в MainThread

Ответы (1 шт):

Автор решения: S. Nick

Я не уверен, что правильно понимаю то, что вы хотите сделать. Но мне кажется, что это может выглядеть примерно так:

import sys
import random
from PyQt5.Qt import *


r = lambda : random.randint(0, 255)
r255 = lambda : (r(), r(), r())


class Scene(QGraphicsScene):
    def __init__(self):
        super().__init__()
        
        # (добавляем много объектов....)
        for i in range(400):
            item = QGraphicsEllipseItem()
            item.setRect(0, 0, r(), r())
            item.setBrush(QColor(*r255()))
            item.setPos(r()*5, r()*5)
            self.addItem(item)


class CalcThread(QThread):
    change_r = pyqtSignal(int, object)
   
    def __init__(self, _items, parent=None):
        super(CalcThread, self).__init__(parent)
        self._items = _items

    def run(self):
        while True:
            #  Долгие расчеты новых параметров для объектов сцены..
            for i in range(1, self._items):
                r = lambda : random.randint(0, 255)
                self.change_r.emit(i, r)
                self.msleep(100)


class Widget_GraphSurface(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(1000, 600)

        self.mainScene = Scene()                    
        
        pixmap = QPixmap("hand128.png")
        pixmap_item = QGraphicsPixmapItem(pixmap)
        pixmap_item.setFlags(
            pixmap_item.flags()
            | QGraphicsItem.ItemIsSelectable
            | QGraphicsItem.ItemIsMovable
        )
        pixmap_item.setPos(700, 700)    
        self.mainScene.addItem(pixmap_item) 
        
        self.ggv_main = QGraphicsView(self.mainScene)
        
        layout = QGridLayout(self)
        layout.addWidget(self.ggv_main, 0, 0)        

        _items = len(self.mainScene.items())
        self.mThread = CalcThread(_items)
        self.mThread.change_r.connect(self.change_item)
        self.mThread.start()

    def change_item(self, i, r):
        item = self.mainScene.items()[i]
        item.setRect(0, 0, r(), r())
        item.setPos(r()*7, r()*7)        
    

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    w = Widget_GraphSurface()
    w.show()
    sys.exit(app.exec_())

введите сюда описание изображения


hand128.png

введите сюда описание изображения

→ Ссылка