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

Skip to content

Commit b1efe69

Browse files
authored
Merge pull request #19651 from meeseeksmachine/auto-backport-of-pr-19618-on-v3.4.x
Backport PR #19618 on branch v3.4.x (FIX: make the cache in font_manager._get_font keyed by thread id)
2 parents df253bc + 7dd6029 commit b1efe69

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,6 +3,7 @@
33
import os
44
from pathlib import Path
55
import shutil
6+
import subprocess
67
import sys
78
import warnings
89

@@ -215,3 +216,42 @@ def test_missing_family(caplog):
215216
"findfont: Generic family 'sans' not found because none of the "
216217
"following families were found: this-font-does-not-exist",
217218
]
219+
220+
221+
def _test_threading():
222+
import threading
223+
from matplotlib.ft2font import LOAD_NO_HINTING
224+
import matplotlib.font_manager as fm
225+
226+
N = 10
227+
b = threading.Barrier(N)
228+
229+
def bad_idea(n):
230+
b.wait()
231+
for j in range(100):
232+
font = fm.get_font(fm.findfont("DejaVu Sans"))
233+
font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING)
234+
235+
threads = [
236+
threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,))
237+
for j in range(N)
238+
]
239+
240+
for t in threads:
241+
t.start()
242+
243+
for t in threads:
244+
t.join()
245+
246+
247+
def test_fontcache_thread_safe():
248+
pytest.importorskip('threading')
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)