-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Closed as not planned
Closed as not planned
Copy link
Description
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
Labels
No labels