QThread 终止后加速而不是终止

QThread speeds up after terminating instead of terminating

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

我的问题:

附带说明一下,我知道我没有使用 Signal/Slot 来检查它是否仍应 运行,我只是想了解这种奇怪的行为,以及为什么线程从一开始就不快!某些东西可能阻止了正确的功能并被 .terminate() 功能关闭(?!)...

我的最小工作示例(希望你们有 soundcard/mic 某处):

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 的第一个建议是不是 subclass QThread,而是使用QObject/QThread/moveToThread。我这样做了,请参阅下面的代码,可以看到使用 whileapp.processEvents()whiletime.sleep(0.1) 都可以解决问题,但要使其响应无论如何你都必须使用 app.processEvents(),所以这就足够了。单独 pass 语句消耗了大量 CPU 处理能力,导致 7-10 fps,但如果你 thread.terminate() 这个线程,一切仍然 运行s.

我另外添加了一个跟踪,在哪个线程上发生了什么,回调总是在一个单独的线程上,不管你使用哪个回调(在任何 class 之外,在 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_()

您的代码中存在一些小问题,所有问题都与 "low" 帧速率一致,主要是因为您使用的块大小为 4096(如果您想要频繁更新,该值太高)并且还尝试过快地更新 GUI ,而 也在处理数据。

在我这种稍微老一点的电脑上,你的代码一开始录制就完全挂了界面,不杀软件根本停不下来。 您看到的不是线程 "speeding" 启动,而是 QTimer 有更多 "time"(周期)来更频繁地调用其超时。

首先,你不应该使用terminate,但可以使用队列向while循环发送"quit"命令,让它优雅地退出。
然后,最好使用 signals/slots 来检索和处理输出数据,因为它更直观并且可以提高整体 cpu 负载。
最后,如果你想获得接收到的数据的 fps,QTimer 设置为 0 是没有用的(这将使它只有 运行 尽可能快,即使没有必要,使 cpu 尖峰不必要的)。

class Record(QThread):
    audioData = pyqtSignal(object)
    def __init__(self):
        super().__init__()
        self.stopper = queue.Queue()

    def callback(self, indata, frames, time, status):
        self.audioData.emit(indata.copy())

    def run(self):
        with sd.InputStream(samplerate=48000, channels=2, callback=self.callback, blocksize=1024):
            print('Stream started...')
            while True:
                try:
                    if self.stopper.get(timeout=.1):
                        break
                except:
                    pass
        print(self.isRunning(), 'Done?') # never called

    def stop(self):
        self.stopper.put(True)


class Main(QWidget):
    # ...
    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.r = Record()
            self.r.audioData.connect(self.plotData)
            self.r.start()

    def plotData(self, data):
        self.curve1.setData(data[:, 0])
        self.curve2.setData(data[:, 1]-3)

        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)))

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

任何像您使用 InputStream 那样传递回调的操作都可能从单独的线程(不是主线程或您的 QThread)调用提供的方法。现在据我所知,回调是从单独的线程调用还是某种中断是平台相关的,但无论哪种方式,它都在某种程度上独立于您的 QThread 运行,即使该方法存在于 class.

while True: pass 将消耗接近 100% 的 CPU,限制任何其他线程可以做的事情。直到你终止它!这为实际调用回调的任何东西释放了资源以更快地工作。虽然您可能希望音频捕获与您的线程一起被杀死,但很可能它还没有被垃圾收集(并且在处理 C/C++ 包装库时垃圾收集变得复杂,当您有两个时没关系他们![PortAudio 和 Qt] - Python 中的垃圾收集很有可能实际上并没有真正释放你的资源!)

所以这解释了为什么当你终止线程时事情会变得更快。

解决方案是将循环更改为 while True: time.sleep(.1),这将确保它不会不必要地消耗资源!您还可以查看您是否真的需要该线程(取决于 PortAudio 在您的平台上的工作方式)。如果您移动到 ​​signal/slot 体系结构并取消 with 语句(在单独的插槽中管理资源的 open/close ),那也可以工作,因为您不需要有问题的循环完全没有。