PyQt4:如何避免这个竞争条件?QThread在完全启动前断开连接
我遇到了一种情况,我想用一个 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 个回答
你想要立即启动一个线程,并通过信号和槽与 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()
。
正确的做法是为每个操作创建一个新的 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()