PyQt4:如何避免这个竞争条件?QThread在完全启动前断开连接

2 投票
2 回答
707 浏览
提问于 2025-04-17 14:46

我遇到了一种情况,我想用一个 QThread 来在不同的时间运行两个(或更多)独立的方法。比如说,我希望这个 QThread 有时能运行 play() 方法,当我玩完之后,我想把这个 QThread 从这个方法上断开连接,这样我就可以把它连接到其他地方。简单来说,我希望 QThread 能像一个容器,可以用来并行运行我想在主程序中执行的任何东西。

我碰到了一个问题,就是启动 QThread 后立刻断开连接,会导致运行时出现奇怪的行为。在我了解“竞争条件”(race condition)是什么意思之前(或者说在我对多线程的理解还不深的时候),我隐约觉得在断开连接之前,线程并没有完全启动。为了克服这个问题,我在 start()disconnect() 之间加了一个 5 毫秒的暂停,这样就能正常工作了。虽然这样有效,但我知道这并不是正确的做法。

我该如何在不调用 sleep() 的情况下,用一个 QThread 实现这个功能呢?

相关代码片段:

def play(self):

        self.stateLabel.setText("Status: Playback initated ...")

        self.myThread.started.connect(self.mouseRecorder.play)
        self.myThread.start()
        time.sleep(.005)  #This is the line I'd like to eliminate

        self.myThread.started.disconnect()

完整脚本:

class MouseRecord(QtCore.QObject):

    finished = QtCore.pyqtSignal()    

    def __init__(self):

        super(MouseRecord, self).__init__()        

        self.isRecording = False
        self.cursorPath = []

    @QtCore.pyqtSlot()  
    def record(self):

        self.isRecording = True
        self.cursorPath = []

        while(self.isRecording):

            self.cursorPath.append(win32api.GetCursorPos())
            time.sleep(.02)            

        self.finished.emit()

    def stop(self):

        self.isRecording = False

    @QtCore.pyqtSlot()    
    def play(self):

        for pos in self.cursorPath:
            win32api.SetCursorPos(pos)
            time.sleep(.02)        

        print "Playback complete!"
        self.finished.emit()            

class CursorCapture(QtGui.QWidget):

    def __init__(self):

        super(CursorCapture, self).__init__()

        self.mouseRecorder = MouseRecord()

        self.myThread = QtCore.QThread()

        self.mouseRecorder.moveToThread(self.myThread)
        self.mouseRecorder.finished.connect(self.myThread.quit)

        self.initUI()

    def initUI(self):

        self.recordBtn = QtGui.QPushButton("Record")
        self.stopBtn   = QtGui.QPushButton("Stop")
        self.playBtn   = QtGui.QPushButton("Play")        

        self.recordBtn.clicked.connect(self.record)
        self.stopBtn.clicked.connect(self.stop)
        self.playBtn.clicked.connect(self.play)

        self.stateLabel = QtGui.QLabel("Status: Stopped.")

        #Bunch of other GUI initialization ...

    def record(self):

        self.stateLabel.setText("Status: Recording ...")  

        self.myThread.started.connect(self.mouseRecorder.record)
        self.myThread.start()
        time.sleep(.005)        

        self.myThread.started.disconnect()

    def play(self):

        self.stateLabel.setText("Status: Playback initated ...")

        self.myThread.started.connect(self.mouseRecorder.play)
        self.myThread.start()
        time.sleep(.005)

        self.myThread.started.disconnect()

2 个回答

0

你想要立即启动一个线程,并通过信号和槽与 MouseRecorder 实例进行沟通,这个操作是在图形界面线程中进行的。

你通过启动 QThread 来向 MouseRecorder 实例发送信号(你已经设置了信号来触发特定事件)。通常情况下,如果你有一些事情只需要在工作线程中执行一次,你会想使用这种信号和槽的连接。否则,通常会通过任何被 moveToThread 移动到线程中的 QObjects 来进行跨线程通信,依然使用信号和槽。




我会这样写:

class MouseRecord(QtCore.QObject):

    def __init__(self):
        super(MouseRecord, self).__init__()        
        self.isRecording = False
        self.cursorPath = []

    @QtCore.pyqtSlot()  
    def record(self):
        self.isRecording = True
        self.cursorPath = []

        while(self.isRecording):
            #Needed, so that if a sigStop is emitted, self.isRecording will be able to be changed
            QApplication.processEvents()

            self.cursorPath.append(win32api.GetCursorPos())
            time.sleep(.02)   

    @QtCore.pyqtSlot()
    def stop(self):
        self.isRecording = False

    @QtCore.pyqtSlot()    
    def play(self):
        for pos in self.cursorPath:
            win32api.SetCursorPos(pos)
            time.sleep(.02)        
        print "Playback complete!"

class CursorCapture(QtGui.QWidget):

    sigRecord = QtCore.pyqtSignal()
    sigPlay = QtCore.pyqtSignal()
    sigStop = QtCore.pyqtSignal()

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

        self.mouseRecorder = MouseRecord()
        self.myThread = QtCore.QThread()

        self.mouseRecorder.moveToThread(self.myThread)

        self.sigRecord.connect(self.mouseRecorder.record)
        self.sigPlay.connect(self.mouseRecorder.play)
        self.sigStop.connect(self.mouseRecorder.stop)

        self.myThread.start()

        self.initUI()

    def initUI(self):
        self.recordBtn = QtGui.QPushButton("Record")
        self.stopBtn   = QtGui.QPushButton("Stop")
        self.playBtn   = QtGui.QPushButton("Play")        

        self.recordBtn.clicked.connect(self.record)
        self.stopBtn.clicked.connect(self.stop)
        self.playBtn.clicked.connect(self.play)

        self.stateLabel = QtGui.QLabel("Status: Stopped.")

        #Bunch of other GUI initialization ...

    def record(self):
        self.stateLabel.setText("Status: Recording ...")  
        self.sigRecord.emit()

    def play(self):
        self.stateLabel.setText("Status: Playback initated ...")
        self.sigPlay.emit()

    def stop(self):
        self.stateLabel.setText("Status: Recording Stopped...")
        self.sigStop.emit()

这样做可以让 QThread 始终保持运行状态(这没问题,因为它不会做任何事情,除非你告诉它去做),同时你的 MouseRecorder 实例会等待来自图形界面线程的信号。

注意还需要额外调用 QApplication::processEvents()

0

正确的做法是为每个操作创建一个新的 QThread,这样就不需要使用 sleep 和断开连接了。现在,即使你成功去掉了 sleep 的调用,还是可能会出现以下情况:

1) 你运行 play,然后断开了槽(slot)。

2) 在 play 还没有完成之前,你又运行了 record。在这种情况下,之前创建的线程仍在运行,结果是:

如果线程已经在运行,这个函数就什么都不做。

(摘自 文档

不过,如果你接受这种情况,并且想办法防止“播放”和“录制”同时进行,那么你应该按照你自己说的去做:“当我完成播放时,我想要断开连接”。所以,不仅仅是在线程启动后,而是在它完成之后。为此,你可以尝试以下操作:

1) 将 self.mouseRecorder.finished.connect(self.myThread.quit) 改为 self.mouseRecorder.finished.connect(self.threadFinished)

2) 实现:

def threadFinished(self):
        self.myThread.quit()
        self.myThread.started.disconnect()

撰写回答