Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ad7a793

Browse files
tacaswellanntzer
andcommitted
FIX: make the cache in font_manager._get_font keyed by thread id
This prevents segfaults when multiple threads try to manipulate the FT2Font object simultaneously. closes #19560 Co-authored-by: Antony Lee <[email protected]>
1 parent 33c3e72 commit ad7a793

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

lib/matplotlib/font_manager.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
import subprocess
3434
import sys
3535
try:
36+
import threading
3637
from threading import Timer
3738
except ImportError:
39+
import dummy_threading as threading
3840
from dummy_threading import Timer
3941

4042
import matplotlib as mpl
@@ -1394,7 +1396,12 @@ def is_opentype_cff_font(filename):
13941396
return False
13951397

13961398

1397-
_get_font = lru_cache(64)(ft2font.FT2Font)
1399+
@lru_cache(64)
1400+
def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id):
1401+
return ft2font.FT2Font(
1402+
filename, hinting_factor, _kerning_factor=_kerning_factor)
1403+
1404+
13981405
# FT2Font objects cannot be used across fork()s because they reference the same
13991406
# FT_Library object. While invalidating *all* existing FT2Fonts after a fork
14001407
# would be too complicated to be worth it, the main way FT2Fonts get reused is
@@ -1409,8 +1416,10 @@ def get_font(filename, hinting_factor=None):
14091416
filename = _cached_realpath(filename)
14101417
if hinting_factor is None:
14111418
hinting_factor = rcParams['text.hinting_factor']
1419+
# also key on the thread ID to prevent segfaults with multi-threading
14121420
return _get_font(filename, hinting_factor,
1413-
_kerning_factor=rcParams['text.kerning_factor'])
1421+
_kerning_factor=rcParams['text.kerning_factor'],
1422+
thread_id=threading.get_ident())
14141423

14151424

14161425
def _load_fontmanager(*, try_read_cache=True):

lib/matplotlib/tests/test_font_manager.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import os
44
from pathlib import Path
55
import shutil
6+
import subprocess
67
import sys
78
import warnings
89

10+
911
import numpy as np
1012
import pytest
1113

@@ -215,3 +217,41 @@ def test_missing_family(caplog):
215217
"findfont: Generic family 'sans' not found because none of the "
216218
"following families were found: this-font-does-not-exist",
217219
]
220+
221+
222+
def _test_threading():
223+
import threading
224+
from matplotlib.ft2font import LOAD_NO_HINTING
225+
import matplotlib.font_manager as fm
226+
227+
N = 10
228+
b = threading.Barrier(N)
229+
230+
def bad_idea(n):
231+
b.wait()
232+
for j in range(100):
233+
font = fm.get_font(fm.findfont("DejaVu Sans"))
234+
font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING)
235+
236+
threads = [
237+
threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,))
238+
for j in range(N)
239+
]
240+
241+
for t in threads:
242+
t.start()
243+
244+
for t in threads:
245+
t.join()
246+
247+
248+
def test_fontcache_thread_safe():
249+
import inspect
250+
251+
proc = subprocess.run(
252+
[sys.executable, "-c",
253+
inspect.getsource(_test_threading) + '\n_test_threading()']
254+
)
255+
if proc.returncode:
256+
pytest.fail("The subprocess returned with non-zero exit status "
257+
f"{proc.returncode}.")

0 commit comments

Comments
 (0)