diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 1f15820b9a..cd75313337 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -360,25 +360,20 @@ namespace Avalonia.Media source = SystemFontsKey; } - if (!_fontCollections.TryGetValue(source, out fontCollection)) + fontCollection = _fontCollections.GetOrAdd(source, static (key, platformImpl) => { - if (source == SystemFontsKey) + if (key == SystemFontsKey) { - fontCollection = new SystemFontCollection(PlatformImpl); - } - else - { - if (source.IsAbsoluteResm() || source.IsAvares()) - { - fontCollection = new EmbeddedFontCollection(source, source); - } + return new SystemFontCollection(platformImpl); } - if (fontCollection != null) + if (key.IsAbsoluteResm() || key.IsAvares()) { - return _fontCollections.TryAdd(fontCollection.Key, fontCollection); + return new EmbeddedFontCollection(key, key); } - } + + return null!; + }, PlatformImpl); return fontCollection != null; } diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs index ed95773630..ab49e0a30d 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs @@ -1,5 +1,10 @@ using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Avalonia.Media; +using Avalonia.Media.Fonts; using Avalonia.UnitTests; using Xunit; @@ -86,5 +91,59 @@ namespace Avalonia.Base.UnitTests.Media Assert.Equal("DejaVu", FontManager.Current.DefaultFontFamily.Name); } } + + [Fact] + public async Task TryGetGlyphTypeface_Should_Be_Thread_Safe_For_Embedded_Fonts() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var fontManager = FontManager.Current; + var fontCollections = (ConcurrentDictionary) + typeof(FontManager) + .GetField("_fontCollections", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(fontManager)!; + + const string fontUri = + "resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests#Noto Mono"; + var collectionKey = + new Uri("resm:Avalonia.Base.UnitTests.Assets?assembly=Avalonia.Base.UnitTests"); + + // Warm up to validate the font URI is correct. + Assert.True(fontManager.TryGetGlyphTypeface(new Typeface(new FontFamily(fontUri)), out _)); + + const int iterations = 50; + int failures = 0; + + for (int i = 0; i < iterations; i++) + { + // Remove the cached collection so both threads must re-create it. + fontCollections.TryRemove(collectionKey, out _); + + using var barrier = new Barrier(2); + bool r1 = false, r2 = false; + + var t1 = Task.Run(() => + { + barrier.SignalAndWait(); + r1 = fontManager.TryGetGlyphTypeface( + new Typeface(new FontFamily(fontUri)), out _); + }); + + var t2 = Task.Run(() => + { + barrier.SignalAndWait(); + r2 = fontManager.TryGetGlyphTypeface( + new Typeface(new FontFamily(fontUri)), out _); + }); + + await Task.WhenAll(t1, t2); + + if (!r1 || !r2) + Interlocked.Increment(ref failures); + } + + Assert.Equal(0, failures); + } + } } }