在PyQt QThreads中同步活动

时间:2014-01-08 19:02:15

标签: python multithreading pyqt

我正在玩PyQt和QThreads。似乎如果我使用我放在this python fiddle中的代码(注意顶部是来自QtDesigner的自动生成代码),其中循环的当前值都在从属循环中打印线程和控制进度条的循环,然后循环保持同步,程序运行时所有点的值都匹配,进度条准确显示完成从属线程的比例。

在回复下面的评论时,这个程序处于当前状态实际上做了我想要的事情 - 它只是向终端打印出从属线程中循环的值和循环中的值。控制进度条的进度。

然而,注释掉第121行(即如果你没有在进度条循环中打印当前值),当从属线程循环到达时,导致进度条达到100%(即完成300次迭代)〜 130次迭代(即进度条的完成速度提高约100%)。

我做过一些天真愚蠢/错误的事 - 有没有更好的方法来完成我想做的事情?!

1 个答案:

答案 0 :(得分:5)

仅仅因为你有两个循环在不同的线程中进行相同数量的迭代,并不意味着它们将花费相同的时间来完成。通常,每次迭代的内容都需要一些时间来完成,并且由于你的循环正在做不同的事情,它们(通常)会花费不同的时间。

此外,使用Python中的线程,Global-Interpreter-Lock(GIL)阻止线程在多核CPU上同时运行。 GIL负责在线程之间切换,并继续执行,然后切换到另一个,然后是另一个,等等。使用QThreads,这变得更加复杂,因为QThread中的Qt调用可以在没有GIL的情况下运行(所以我理解)但一般的python代码仍将与GIL一起运行。

因为GIL负责处理在任何给定时间运行的线程,所以我甚至看到了两个线程,执行完全相同的操作,以不同的速度运行。 因此,你的两个主题同时完成是完全巧合的!

请注意,由于GIL,两个cpu-instensive任务在单独的线程中运行没有速度优势。为此,您需要使用多处理。但是,如果您想要释放I / O绑定任务(例如通过主线程中的GUI的用户界面,另一个线程中的网络通信,也就是通常花费大量时间等待程序之外的任务的任务)触发某事)然后线程很有用。

所以,希望这有助于解释线程,以及程序中发生的事情。

有几种方法可以做得更好。一种方法是将循环保留在线程中,但删除另一个。然后使用qt signal / slot机制调用MainWindow中的函数,该函数运行曾经存在的循环的一次迭代。这不保证同步,只是你的QThread会先完成(有些东西可能会减慢主线程的速度,以便事件堆积起来,而MainWindow中的函数直到稍后才会运行)。要完成同步,您可以使用threading.Event对象使QThread等待,直到MainWindow中的新函数运行。

示例(未经测试,抱歉,但希望能提出这个想法!):

import threading
#==========================================
class TaskThread(QtCore.QThread):

    setTime = QtCore.pyqtSignal(int,int)
    iteration = QtCore.pyqtSignal(threading.Event, int)

    def run(self):

        self.setTime.emit(0,300)
        for i in range(300):
            time.sleep(0.05)
            event = threading.Event()
            self.iteration.emit(event, i)
            event.wait()

#==========================================
class MainWindow(QtGui.QMainWindow):

    _uiform = None

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self,parent)
        self._uiform = Ui_MainWindow()
        self._uiform.setupUi(self)
        self._uiform.runButton.clicked.connect(self.startThread)


    def startThread(self):
        self._uiform.progressBar.setRange(0,0)
        self.task = TaskThread()
        self.task.setTime.connect(self.changePB)
        self.task.iteration.connect(self.update_prog_bar)
        self.task.start()

    @QtCore.pyqtSlot(int,int)
    def changePB(self, c, t):
        self.proportionFinished = int(math.floor(100*(float(c)/t)))
        self._uiform.progressBar.setValue(self.proportionFinished)

        self._uiform.progressBar.setRange(0,300)
        self._uiform.progressBar.setValue(0)

    @QtCore.pyqtSlot(threading._Event,int)
    def update_prog_bar(self,event, i)
        self._uiform.progressBar.setValue(i+1)
        print i
        event.set()

请注意,使用@QtCore.pyqtSlot()装饰器是因为记录了问题here。简而言之,当你使用signal.connect(my_function)时,你省略了第二个参数,它确定了槽的行为(是否在调用signal.emit()时立即执行,或者一旦控制返回到执行,它是否被执行事件循环(又名,放在队列中以便稍后运行))。默认情况下,此连接的可选参数尝试自动决定通常使用哪种类型的连接(请参阅here)。但是,如果连接是在它知道它是线程之间的连接之前进行的,并且“槽”没有*使用@pyqtSlot显式定义为槽,则pyQT会混淆!

有关装饰器的其他信息:考虑装饰器的最简单方法是将函数包装到另一个函数中的简写。装饰器用自己的函数替换你定义的函数,这个新函数通常在某个时候使用你原来的函数。所以在@pyqtSlot情况下,信号发射实际上调用了由@pyqtSlot生成的pyqt函数,并且该函数最终调用您编写的原始函数。 @pyqtSlot装饰器接受与插槽的参数类型匹配的参数这一事实是pyqt装饰器的实现细节,并不代表装饰器。它只是声明您的插槽期望通过连接信号传递指定类型的数据。