我对QThread的行为完全感到困惑。我的想法是在qthread中获取一些音频信号,将其保存在python queue对象中,并使用QTimer读取队列并使用pyqtgraph对其进行绘制。但是,它只能以6-7 fps的速度运行。但是,当我使用.terminate()终止线程时,该线程实际上并没有终止,而是达到了> 100 fps的速度,这正是我真正想要的。

我的问题:

  • 为什么QThread不会终止/中止/关闭...?
  • .terminate()实际在做什么?
  • 什么会减慢正常的thread.start()

  • 附带一提,我知道我没有在使用信号/插槽来检查它是否仍然应该运行,我只是想了解这种奇怪的行为,以及为什么线程从一开始就不快速!某些东西可能会阻止适当的功能,并被.terminate()函数关闭(?!)...

    我的最小工作示例(希望你们在某处有声卡/麦克风):
    from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton
    from PyQt5.QtCore import QThread, QTimer
    import sounddevice as sd
    import queue
    import pyqtgraph as pg
    import numpy as np
    import time
    
    class Record(QThread):
        def __init__(self):
            super().__init__()
            self.q = queue.Queue()
    
        def callback(self, indata, frames, time, status):
            self.q.put(indata.copy())
    
        def run(self):
            with sd.InputStream(samplerate=48000, device=1, channels=2, callback=self.callback, blocksize=4096):
                print('Stream started...')
                while True:
                    pass
    
            print(self.isRunning(), 'Done?') # never called
    
    class Main(QWidget):
        def __init__(self):
            super().__init__()
            self.recording = False
            self.r = None
            self.x = 0
            self.times = list(range(10))
    
            self.setWindowTitle("Record Audio Tester")
    
            self.l = QGridLayout()
            self.setLayout(self.l)
    
            self.pl = pg.PlotWidget(autoRange=False)
            self.curve1 = self.pl.plot(np.zeros(8000))
            self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
    
            self.l.addWidget(self.pl)
    
            self.button_record = QPushButton("Start recording")
            self.button_record.clicked.connect(self.record)
            self.l.addWidget(self.button_record)
    
        def record(self):
            if self.recording and self.r is not None:
                self.button_record.setText("Start recording")
                self.recording = False
                self.r.terminate()
    
            else:
                self.button_record.setText("Stop recording")
                self.recording = True
    
                self.r = Record()
                self.r.start()
    
                self.t = QTimer()
                self.t.timeout.connect(self.plotData)
                self.t.start(0)
    
        def plotData(self):
            self.times = self.times[1:]
            self.times.append(time.time())
    
            fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
            self.setWindowTitle("{:d} fps...".format(int(fps)))
    
            if self.r.q.empty():
                return
    
            d = self.r.q.get()
    
            self.curve1.setData(d[:, 0])
            self.curve2.setData(d[:, 1]-3)
    
    
    if __name__ == '__main__':
        app = QApplication([])
    
        w = Main()
        w.show()
    
        app.exec_()
    

    编辑1

    第一个建议@Dennis Jensen是而不是子类QThread,而是使用QObject / QThread / moveToThread。我这样做了,请参见下面的代码,然后可以看到使用while以及仅使用app.processEvents()whiletime.sleep(0.1)都解决了问题,但是要使其响应,您无论如何都必须使用app.processEvents(),所以这就足够了。仅pass语句会消耗大量CPU处理能力,从而导致7-10 fps,但是如果您对这个线程进行thread.terminate(),则所有内容仍会运行。

    我还添加了一个跟踪,在哪个线程上发生了什么,并且回调始终在单独的线程上,无论您使用哪个回调(QObject或主线程中的任何类之外),都表示来自@three_pineapples的答案正确。
    from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton, QCheckBox
    from PyQt5.QtCore import QThread, QTimer, QObject, pyqtSignal, pyqtSlot
    import threading
    import sounddevice as sd
    import queue
    import pyqtgraph as pg
    import numpy as np
    import time
    
    q = queue.Queue()
    
    # It does not matter at all where the callback is,
    # it is always on its own thread...
    def callback(indata, frames, time, status):
            print("callback", threading.get_ident())
            # print()
            q.put(indata.copy())
    
    class Record(QObject):
        start = pyqtSignal(str)
        stop = pyqtSignal()
        data = pyqtSignal(np.ndarray)
    
        def __init__(self, do_pass=False, use_terminate=False):
            super().__init__()
            self.q = queue.Queue()
            self.r = None
            self.do_pass = do_pass
            self.stop_while = False
            self.use_terminate = use_terminate
            print("QObject -> __init__", threading.get_ident())
    
        def callback(self, indata, frames, time, status):
            print("QObject -> callback", threading.get_ident())
            self.q.put(indata.copy())
    
        @pyqtSlot()
        def stopWhileLoop(self):
            self.stop_while = True
    
        @pyqtSlot()
        def run(self, m='sth'):
            print('QObject -> run', threading.get_ident())
    
            # Currently uses a callback outside this QObject
            with sd.InputStream(device=1, channels=2, callback=callback) as stream:
                # Test the while pass function
                if self.do_pass:
                    while not self.stop_while:
                        if self.use_terminate: # see the effect of thread.terminate()...
                            pass # 7-10 fps
                        else:
                            app.processEvents() # makes it real time, and responsive
    
                    print("Exited while..")
                    stream.stop()
    
                else:
                    while not self.stop_while:
                        app.processEvents() # makes it responsive to slots
                        time.sleep(.01) # makes it real time
    
                    stream.stop()
    
            print('QObject -> run ended. Finally.')
    
    class Main(QWidget):
        def __init__(self):
            super().__init__()
            self.recording = False
            self.r = None
            self.x = 0
            self.times = list(range(10))
            self.q = queue.Queue()
    
            self.setWindowTitle("Record Audio Tester")
    
            self.l = QGridLayout()
            self.setLayout(self.l)
    
            self.pl = pg.PlotWidget(autoRange=False)
            self.curve1 = self.pl.plot(np.zeros(8000))
            self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
    
            self.l.addWidget(self.pl)
    
            self.button_record = QPushButton("Start recording")
            self.button_record.clicked.connect(self.record)
            self.l.addWidget(self.button_record)
    
            self.pass_or_sleep = QCheckBox("While True: pass")
            self.l.addWidget(self.pass_or_sleep)
    
            self.use_terminate = QCheckBox("Use QThread terminate")
            self.l.addWidget(self.use_terminate)
    
            print("Main thread", threading.get_ident())
    
        def streamData(self):
            self.r = sd.InputStream(device=1, channels=2, callback=self.callback)
    
        def record(self):
            if self.recording and self.r is not None:
                self.button_record.setText("Start recording")
                self.recording = False
                self.r.stop.emit()
    
                # And this is where the magic happens:
                if self.use_terminate.isChecked():
                    self.thr.terminate()
    
            else:
                self.button_record.setText("Stop recording")
                self.recording = True
    
                self.t = QTimer()
                self.t.timeout.connect(self.plotData)
                self.t.start(0)
    
                self.thr = QThread()
                self.thr.start()
    
                self.r = Record(self.pass_or_sleep.isChecked(), self.use_terminate.isChecked())
                self.r.moveToThread(self.thr)
                self.r.stop.connect(self.r.stopWhileLoop)
                self.r.start.connect(self.r.run)
                self.r.start.emit('go!')
    
        def addData(self, data):
            # print('got data...')
            self.q.put(data)
    
        def callback(self, indata, frames, time, status):
            self.q.put(indata.copy())
            print("Main thread -> callback", threading.get_ident())
    
    
        def plotData(self):
            self.times = self.times[1:]
            self.times.append(time.time())
    
            fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
            self.setWindowTitle("{:d} fps...".format(int(fps)))
    
            if q.empty():
                return
    
            d = q.get()
            # print("got data ! ...")
    
            self.curve1.setData(d[:, 0])
            self.curve2.setData(d[:, 1]-1)
    
    
    if __name__ == '__main__':
        app = QApplication([])
    
        w = Main()
        w.show()
    
        app.exec_()
    

    编辑2

    这里的代码不使用QThread环境,并且可以按预期工作!
    from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton, QCheckBox
    from PyQt5.QtCore import QTimer
    import threading
    import sounddevice as sd
    import queue
    import pyqtgraph as pg
    import numpy as np
    import time
    
    
    class Main(QWidget):
        def __init__(self):
            super().__init__()
            self.recording = False
            self.r = None
            self.x = 0
            self.times = list(range(10))
            self.q = queue.Queue()
    
            self.setWindowTitle("Record Audio Tester")
    
            self.l = QGridLayout()
            self.setLayout(self.l)
    
            self.pl = pg.PlotWidget(autoRange=False)
            self.curve1 = self.pl.plot(np.zeros(8000))
            self.curve2 = self.pl.plot(np.zeros(8000)-1, pen=pg.mkPen("y"))
    
            self.l.addWidget(self.pl)
    
            self.button_record = QPushButton("Start recording")
            self.button_record.clicked.connect(self.record)
            self.l.addWidget(self.button_record)
    
            print("Main thread", threading.get_ident())
    
        def streamData(self):
            self.r = sd.InputStream(device=1, channels=2, callback=self.callback)
            self.r.start()
    
        def record(self):
            if self.recording and self.r is not None:
                self.button_record.setText("Start recording")
                self.recording = False
                self.r.stop()
    
            else:
                self.button_record.setText("Stop recording")
                self.recording = True
    
                self.t = QTimer()
                self.t.timeout.connect(self.plotData)
                self.t.start(0)
    
                self.streamData()
    
        def callback(self, indata, frames, time, status):
            self.q.put(indata.copy())
            print("Main thread -> callback", threading.get_ident())
    
    
        def plotData(self):
            self.times = self.times[1:]
            self.times.append(time.time())
    
            fps = 1 / (np.diff(np.array(self.times)).mean() + 1e-5)
            self.setWindowTitle("{:d} fps...".format(int(fps)))
    
            if self.q.empty():
                return
    
            d = self.q.get()
            # print("got data ! ...")
    
            self.curve1.setData(d[:, 0])
            self.curve2.setData(d[:, 1]-1)
    
    
    if __name__ == '__main__':
        app = QApplication([])
    
        w = Main()
        w.show()
    
        app.exec_()
    

    最佳答案

    问题是由于线程中的while True: pass行。要了解原因,您需要了解PortAudio(由sounddevice包装的库)如何工作。

    像您对InputStream所做的一样,传递回调的任何东西都可能从单独的线程(而不是主线程或QThread)调用提供的方法。现在,根据我的判断,是从单独的线程还是通过某种中断调用回调取决于平台,但是无论哪种方法,回调方法的运行都与QThread无关,即使该方法存在于该类中。
    while True: pass将消耗接近100%的CPU,从而限制了其他线程可以执行的操作。那是直到您终止它!这样可以释放资源用于实际上调用回调以更快地工作。尽管您可能会期望音频捕获与线程一起被杀死,但是可能还没有对其进行垃圾回收(处理C / C++包装的库时,垃圾回收变得很复杂,而当您拥有两个时,不要紧![PortAudio和[Qt]-而且很有可能Python中的垃圾回收实际上并不会在您的情况下真正释放资源!)

    因此,这解释了为什么终止线程时事情变得更快。

    解决方案是将您的循环更改为while True: time.sleep(.1),这将确保它不会不必要地消耗资源!您还可以调查您是否真的需要该线程(取决于PortAudio在您的平台上的工作方式)。如果您转为信号/插槽架构,并且放弃了with语句(在单独的插槽中管理资源的打开/关闭),那么它也将起作用,因为您根本不需要有问题的循环。

    关于python - QThread在终止而不是终止后加速,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/58559303/

    10-10 21:15
    查看更多