QThread ускоряется после завершения вместо завершения - PullRequest
0 голосов
/ 25 октября 2019

Я полностью сбит с толку поведением QThread. Моя идея состоит в том, чтобы получить некоторый аудиосигнал в qthread, сохранить его в объекте python queue и с помощью QTimer я читаю очередь и строю его с помощью pyqtgraph. Это работает, однако, только на скорости около 6-7 кадров в секунду. Однако, когда я использую .terminate() для завершения потока, поток на самом деле НЕ завершается, а скорее достигает скорости> 100 кадров в секунду, именно то, что я на самом деле хотел.

Мои проблемы:

  • почему 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_()

edit 1

Первым предложением @Dennis Jensen было не подкласс QThread, а скорее использование QObject / QThread / moveToThread. Я сделал это, см. Код ниже, и можно увидеть, что проблема устранена с использованием while и просто app.processEvents() или while с time.sleep(0.1), но для ответа вы должны будете использовать в любом случае app.processEvents()так что этого достаточно. Один оператор pass потребляет много ресурсов процессора, что приводит к 7-10 кадрам в секунду, но если вы 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_()

edit 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_()

Ответы [ 2 ]

2 голосов
/ 26 октября 2019

Проблема связана со строкой while True: pass в вашей теме. Чтобы понять почему, вам нужно понять, как работает PortAudio (библиотека, обернутая sounddevice).

Все, что прошло обратный вызов, как вы делаете с InputStream, вероятно, вызывает указанный метод из отдельного потока (неосновная нить или ваша QThread). Теперь из того, что я могу сказать, вызывается ли обратный вызов из отдельного потока или какое-то прерывание, зависит от платформы, но в любом случае он работает несколько независимо от вашего QThread, даже если метод существует внутри этого класса.

while True: pass будет потреблять около 100% вашего процессора, ограничивая возможности любого другого потока. То есть, пока вы не прекратите это! Что освобождает ресурсы для того, что фактически вызывает обратный вызов, чтобы работать быстрее. Хотя вы можете ожидать, что аудиозахват будет уничтожен вместе с вашим потоком, есть вероятность, что он еще не был собран сборщиком мусора (а сборка мусора усложняется при работе с обернутыми библиотеками C / C ++, не говоря уже о двух из них! и Qt] - И есть большая вероятность, что сборка мусора в Python может вообще не освободить ресурсы в вашем случае!)

Так что это объясняет, почему все становится быстрее, когда вы завершаете поток.

Решение состоит в том, чтобы изменить ваш цикл на while True: time.sleep(.1), что обеспечит ненужное использование ресурсов! Вы также можете посмотреть, нужен ли вам этот поток вообще (в зависимости от того, как PortAudio работает на вашей платформе). Если вы перейдете к архитектуре сигнал / слот и покончили с оператором with (управляющим открытием / закрытием ресурса в отдельных слотах), это также сработает, поскольку вам вообще не понадобится проблемный цикл.

0 голосов
/ 25 октября 2019

Есть некоторые небольшие проблемы в вашем коде, все они согласуются с «низкой» частотой кадров, в основном из-за того, что вы используете размер блока 4096 (который слишком высок, если вы хотите частые обновления), а также пытаетесьобновлять графический интерфейс слишком быстро , в то время как также обрабатывает данные.

На немного старом компьютере, как мой, ваш код полностью зависает, как только я начинаю запись, и остановка практически невозможна, еслине убивая приложение вообще. То, что вы видите, - это не ускорение потока, а QTimer, у которого гораздо больше «времени» (циклов) для более частого вызова тайм-аута.

Прежде всего, вы не должны использовать terminate, но, возможно, используйте Очередь, чтобы отправить команду «выход» в цикл while, позволяя ей корректно завершиться.
Тогда лучше использовать сигналы / слоты для извлечения и обработки выходных данных, поскольку они более интуитивно понятны иулучшает общую загрузку процессора.
Наконец, если вы хотите получить частоту кадров полученных данных, нет смысла в QTimer, установленном в 0 (что заставит его работать только настолько быстро, насколько это возможно, даже если в этом нет необходимости, делая процессорСпайк без необходимости).

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

...