From 7dd60291ed7fc1f9561001fdd415eb726d156a9c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 5 Mar 2021 22:01:53 -0500 Subject: [PATCH] Backport PR #19618: FIX: make the cache in font_manager._get_font keyed by thread id --- lib/matplotlib/font_manager.py | 13 ++++++-- lib/matplotlib/tests/test_font_manager.py | 40 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 63c97a04d393..f7a8bdb87225 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -33,8 +33,10 @@ import subprocess import sys try: + import threading from threading import Timer except ImportError: + import dummy_threading as threading from dummy_threading import Timer import matplotlib as mpl @@ -1394,7 +1396,12 @@ def is_opentype_cff_font(filename): return False -_get_font = lru_cache(64)(ft2font.FT2Font) +@lru_cache(64) +def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): + return ft2font.FT2Font( + filename, hinting_factor, _kerning_factor=_kerning_factor) + + # FT2Font objects cannot be used across fork()s because they reference the same # FT_Library object. While invalidating *all* existing FT2Fonts after a fork # 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): filename = _cached_realpath(filename) if hinting_factor is None: hinting_factor = rcParams['text.hinting_factor'] + # also key on the thread ID to prevent segfaults with multi-threading return _get_font(filename, hinting_factor, - _kerning_factor=rcParams['text.kerning_factor']) + _kerning_factor=rcParams['text.kerning_factor'], + thread_id=threading.get_ident()) def _load_fontmanager(*, try_read_cache=True): diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 88e5fed63c76..1e8252abb76c 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -3,6 +3,7 @@ import os from pathlib import Path import shutil +import subprocess import sys import warnings @@ -215,3 +216,42 @@ def test_missing_family(caplog): "findfont: Generic family 'sans' not found because none of the " "following families were found: this-font-does-not-exist", ] + + +def _test_threading(): + import threading + from matplotlib.ft2font import LOAD_NO_HINTING + import matplotlib.font_manager as fm + + N = 10 + b = threading.Barrier(N) + + def bad_idea(n): + b.wait() + for j in range(100): + font = fm.get_font(fm.findfont("DejaVu Sans")) + font.set_text(str(n), 0.0, flags=LOAD_NO_HINTING) + + threads = [ + threading.Thread(target=bad_idea, name=f"bad_thread_{j}", args=(j,)) + for j in range(N) + ] + + for t in threads: + t.start() + + for t in threads: + t.join() + + +def test_fontcache_thread_safe(): + pytest.importorskip('threading') + import inspect + + proc = subprocess.run( + [sys.executable, "-c", + inspect.getsource(_test_threading) + '\n_test_threading()'] + ) + if proc.returncode: + pytest.fail("The subprocess returned with non-zero exit status " + f"{proc.returncode}.")