Description
Although we are very much no thread safe, that this is a "segfault" failure so I think it should be addressed. Users should be able to take their toes off with threads, not their whole leg.
I discovered this in an application my group develops at BNL which is more-or-less
import faulthandler
import io
import time
import threading
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvas
faulthandler.enable()
fig = Figure()
cav = FigureCanvas(fig)
ax = fig.subplots()
ln, = ax.plot(range(5), label='bob')
ax.legend()
def bad_idea(fig, n):
time.sleep(1)
print(f'go {n}')
for j in range(100):
# time.sleep(.01)
fig.gca().legend()
fig.tight_layout()
print(f'done {n}')
fig.savefig(io.BytesIO())
threads = [threading.Thread(target=bad_idea, name=f'bad_thread_{j}', args=(fig, j)).start()
for j in range(10)]
for j in range(5):
fig.canvas.draw()
but maybe a bit less over-the-top (we only have 1 background thread and it only fires occasionally), but the goal here is to make sure we always lose the race. Tracing this back I eventually got to this being related to fonts and reduced the reproducing case to:
import faulthandler
import time
import threading
import matplotlib.font_manager as fm
from matplotlib.ft2font import LOAD_NO_HINTING
faulthandler.enable()
def bad_idea(fm, n):
time.sleep(1)
print(f"go {n}")
for j in range(100):
font = fm.get_font(fm.findfont("Dej
DC01
aVu Sans"))
# font.clear()
# font.set_size(12, 72.0)
font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING)
print(f"done {n}")
threads = [
threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(fm, j)).start()
for j in range(10)
]
If you turn down the number of threads you can also get various errors out of freetype
$ python /tmp/bad_font.py
go 0
go 2
go 1
Exception in thread bad_thread_2:
Traceback (most recent call last):
File "/usr/lib/python3.9/threading.py", line 954, in _bootstrap_inner
self.run()
File "/usr/lib/python3.9/threading.py", line 892, in run
self._target(*self._args, **self._kwargs)
File "/tmp/bad_font.py", line 17, in bad_idea
font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING)
RuntimeError: In set_text: Could not get glyph (error code 0x12)
Exception in thread bad_thread_0:
Traceback (most recent call last):
File "/usr/lib/python3.9/threading.py", line 954, in _bootstrap_inner
self.run()
File "/usr/lib/python3.9/threading.py", line 892, in run
self._target(*self._args, **self._kwargs)
File "/tmp/bad_font.py", line 17, in bad_idea
font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING)
RuntimeError: In set_text: Could not load glyph (error code 0x14)
done 1
vs
$ python /tmp/bad_font.py
go 1
go 2
go 0
Exception in thread bad_thread_2:
Traceback (most recent call last):
File "/usr/lib/python3.9/threading.py", line 954, in _bootstrap_inner
Fatal Python error: Segmentation fault
Thread 0x00007fefdac67640 (most recent call first):
File "/usr/lib/python3.9/threading.py", line 1214 in invoke_excepthook
File "/usr/lib/python3.9/threading.py", line 956 in _bootstrap_inner
File "/usr/lib/python3.9/threading.py", line 912 in _bootstrap
Current thread 0x00007fefd8466640 (most recent call first):
File "/tmp/bad_font.py", line 17 in bad_idea
File "/usr/lib/python3.9/threading.py", line 892 in run
File "/usr/lib/python3.9/threading.py", line 954 in _bootstrap_inner
File "/usr/lib/python3.9/threading.py", line 912 in _bootstrap
Thread 0x00007fefd5c65640 (most recent call first):
File "/usr/lib/python3.9/posixpath.py", line 167 in islink
File "/usr/lib/python3.9/posixpath.py", line 425 in _joinrealpath
File "/usr/lib/python3.9/posixpath.py", line 391 in realpath
File "/home/tcaswell/source/p/matplotlib/mpl-main/lib/matplotlib/font_manager.py", line 1228 in findfont
File "/tmp/bad_font.py", line 14 in bad_idea
File "/usr/lib/python3.9/threading.py", line 892 in run
File "/usr/lib/python3.9/threading.py", line 954 in _bootstrap_inner
File "/usr/lib/python3.9/threading.py", line 912 in _bootstrap
Thread 0x00007fefe6309740 (most recent call first):
File "/usr/lib/python3.9/threading.py", line 1428 in _shutdown
Segmentation fault (core dumped)
I bisected this back to https://github.com/matplotlib/matplotlib/pull/15104/files and 97477d7 specifically.
I think we have a couple of options here:
- revert that PR temporairily (maybe we want to do this for 3.4?)
- add some locking at the c-level in the ft2font
- add some locking at the python level
- make the font cache a thread-local
I have some code to implement locking, but it gets a bit gnarly (as I have both a lock and a semaphore to try and make sure we always use fonts safely). Sorting out how to make sure we always lock around font access is complicated because we cache the font objects in at least 2 places
matplotlib/lib/matplotlib/font_manager.py
Lines 1397 to 1413 in a510e32
matplotlib/lib/matplotlib/_mathtext.py
Lines 222 to 253 in a510e32
We also have some existing locking around the font cache
matplotlib/lib/matplotlib/backends/backend_agg.py
Lines 72 to 83 in a510e32
Matplotlib version
- Operating system: linux
- Matplotlib version (
import matplotlib; print(matplotlib.__version__)
): 3.3.x + - Matplotlib backend (
print(matplotlib.get_backend())
): any - Python version: 3.7-3.9
- Jupyter version (if applicable): N/A
- Other libraries: