PyQt5:Python 当通过来自另一个线程的信号发送像素图时,SIGSEGV *有时* 崩溃

PyQt5: Python crashes with SIGSEGV *sometimes* when sending pixmap via a signal from another thread

背景和问题

我正在尝试处理来自相机的流数据。 Python 尽管此消息不断崩溃:

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

当发射包含图像的信号时,有时 会发生崩溃。

我的代码(如下所示)遵循以下过程:

  1. 一个名为 CameraThreadQObject 在 GUI 中被实例化并且被 QThread.
  2. 运行
  3. CameraThread 实例化一个class IngestManager 给数据源。数据源会反复调用IngestManagerwrite()方法,提供数据。
  4. 工作线程处理数据并通过回调方法将其发送回 IngestManager frame_callback
  5. IngestManager 发出信号。这是它崩溃的地方有时

我的尝试/观察结果

我尝试了几种方法来修复它,包括将 pyqtSignal 传递给工作线程本身。我还认为有时线程会同时完成和发出,但我不确定如何解决这个问题。

我与 GUI 交互越多,例如快速按下虚拟按钮,崩溃发生得越快。如果我不与 UI 交互,它几乎永远不会发生(但它仍然会发生)。我想我可能需要某种锁。

如何开始解决这个问题?


这是连接到数据源、处理数据和发出信号的代码。

import io
import threading

from PIL import Image
from PIL.ImageQt import ImageQt
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QPixmap


class CameraThread(QObject):
    signal_new_frame = pyqtSignal(QPixmap)

    def __init__(self, parent=None):
        QObject.__init__(self, parent)

    @pyqtSlot()
    def run(self):
        with DataSource() as source:
            output = IngestManager(frame_signal=self.signal_new_frame)

            # The source continuously calls IngestManager.write() with new data
            source.set_ingest(output)


class IngestManager(object):
    """Manages incoming data stream from camera."""

    def __init__(self, frame_signal):
        self.frame_signal = frame_signal

        # Construct a pool of 4 image processors along with a lock to control access between threads
        self.lock = threading.Lock()
        self.pool = [ImageProcessor(self) for _ in range(4)]
        self.processor = None  # First "frame" intentionally dropped, else thread would see garbled data at start

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):  # Frame detected
            if self.processor:
                self.processor.event.set()  # Let waiting processor thread know a frame is here

            with self.lock:
                if self.pool:
                    self.processor = self.pool.pop()
                else:
                    # All threads popped and busy. No choice but to skip frame.
                    self.processor = None

        if self.processor:
            self.processor.stream.write(buf)  # Feed frame data to current processor

    def frame_callback(self, image):
        print('Frame processed. Emitting.')
        self.frame_signal.emit(image)


class ImageProcessor(threading.Thread):
    def __init__(self, owner: IngestManager):
        super(ImageProcessor, self).__init__()

        # Data Stuff
        self.stream = io.BytesIO()

        # Thread stuff
        self.event = threading.Event()
        self.owner = owner
        self.start()

    def run(self):
        while True:
            if self.event.wait(1):
                pil_image = Image.open(self.stream)
        
                # Image is processed here, then sent back
                # ...
                # ...
        
                q_image = ImageQt(pil_image)
                q_pixmap = QPixmap.fromImage(q_image)
        
                self.owner.frame_callback(q_pixmap)

                # Reset the stream and event
                self.stream.seek(0)
                self.stream.truncate()
                self.event.clear()
                
                # Return to available pool
                with self.owner.lock:
                    self.owner.pool.append(self)

上面的代码是这样使用的:

from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread
from Somewhere import Ui_MainWindow

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)
        self.showFullScreen()

        # Task and thread instantiated. Task is then assigned to thread.
        self._thread = QThread()
        self._cam_prev = CameraThread()
        self._cam_prev.moveToThread(self._thread)

        # Signals are connected.
        self._cam_prev.signal_new_frame.connect(self.update_image)  # UI signal to show image on QLabel
        self._thread.started.connect(self._cam_prev.run)  # Trigger task's run() when thread is ready
        self._thread.start()

    @pyqtSlot(QPixmap)
    def update_image(self, q_pixmap: QPixmap):
        self.q_cam_preview.setPixmap(q_pixmap)

首先,在主线程外使用QPixmap是不安全的。所以你应该改用QImage

其次,ImageQt 共享传递给它的 Image 的缓冲区。因此,如果在 Qt 图像仍然存在时删除缓冲区,很可能会发生崩溃。如果您不能长时间保留 PIL 图像,您可能需要 copy the Qt image 来防止这种情况发生。