Skip to content

[Bug]: QtAgg blitting with multithreading throwing SIGSEGV with endPaint() called with active painter #24590

@jamesbraza

Description

@jamesbraza

Bug summary

I am trying to generalize Faster rendering by using blitting to several artists being called on a thread. When I call the repro script, I get:

QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?

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

Or sometimes a different error:

QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
QPainter::begin: A paint device can only be painted by one painter at a time.
QPainter::setCompositionMode: Painter not active

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

It looks like there is a race condition (not sure what's going wrong exactly) when blitting with multithreading + lock protection.

Code for reproduction

import random
import time
from collections.abc import Callable
from threading import Lock, Thread

import matplotlib
import matplotlib.artist
import matplotlib.axes
import matplotlib.figure
import matplotlib.pyplot as plt

matplotlib.use("QtAgg")


class LivePlotter:
    """Show a live plot and expose the ability to add points on demand."""

    fig: matplotlib.figure.Figure
    ax: matplotlib.axes.Axes

    def __init__(self):
        # NOTE: matplotlib is not thread safe, SEE:
        # https://matplotlib.org/stable/users/faq/howto_faq.html#work-with-threads
        self._plot_lock = Lock()
        self.fig, self.ax = plt.subplots()
        self.ax.set_xlim(0, 1)
        self.ax.set_ylim(0, 1)
        self.ax.grid()
        self.fig.tight_layout()
        plt.show(block=False)
        plt.pause(0.1)
        self.bg = self.fig.canvas.copy_from_bbox(self.fig.bbox)
        self._artists: list[matplotlib.artist.Artist] = []

    def _draw_animated(self) -> None:
        """Restore the bg, draw all artists, and blit."""
        self.fig.canvas.restore_region(self.bg)
        for a in self._artists:
            self.fig.draw_artist(a)
        self.fig.canvas.blit(self.fig.bbox)

    def add_line(self) -> Callable[[], None]:
        x_data, y_data = [], []
        with self._plot_lock:
            (line,) = self.ax.plot(x_data, y_data, animated=True, marker=".", lw=2.0)
            self._artists.append(line)
            plt.pause(0.1)
            self._draw_animated()

        def append_point() -> None:
            with self._plot_lock:
                x_data.append(random.random())
                y_data.append(random.random())
                line.set_data(x_data, y_data)
                self._draw_animated()
                self.fig.canvas.flush_events()

        return append_point


def make_points(
    updater: Callable[[], None], num_points: int = 5000, sleep_duration: float = 0.001
) -> None:
    for _ in range(num_points):
        updater()
        time.sleep(sleep_duration)


def main() -> None:
    plotter = LivePlotter()
    updater_1 = plotter.add_line()
    updater_2 = plotter.add_line()

    threads = [
        Thread(target=make_points, args=(updater_1,), daemon=False),
        Thread(target=make_points, args=(updater_2,), daemon=False),
    ]
    for thread in threads:
        thread.start()

    plt.show()

    for thread in threads:
        thread.join()


if __name__ == "__main__":
    main()

Actual outcome

I get a segfault and the QBackingStore sends the endPaint() called with active painter message seen in the summary.

Expected outcome

I expect it to work properly and render all points.

Additional information

No response

Operating system

macOS Monterey version 12.6

Matplotlib Version

3.6.2

Matplotlib Backend

QtAgg

Python version

3.10.8

Jupyter version

b/a

Installation

pip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy