Skip to content

Commit 354b86f

Browse files
authored
🛡️ Add concurrency checks to UnitValueAbbreviationLookup (#1160)
Refs #919 Use `ConcurrentDictionary` for lookups. Use `IReadOnlyList` and treat list instances as immutable. Reduce `ToList()` allocations. Use `Lazy` to compute all abbreviations.
1 parent c14fbc9 commit 354b86f

File tree

3 files changed

+80
-39
lines changed

3 files changed

+80
-39
lines changed

UnitsNet.Tests/UnitAbbreviationsCacheTests.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Globalization;
7-
using System.Linq;
6+
using UnitsNet.Tests.CustomQuantities;
87
using UnitsNet.Units;
98
using Xunit;
109
using Xunit.Abstractions;
@@ -274,6 +273,33 @@ public void MapUnitToDefaultAbbreviation_GivenCustomAbbreviation_SetsAbbreviatio
274273
Assert.Equal("1 m^2", Area.FromSquareMeters(1).ToString(newZealandCulture));
275274
}
276275

276+
[Fact]
277+
public void MapUnitToAbbreviation_DoesNotInsertDuplicates()
278+
{
279+
var cache = new UnitAbbreviationsCache();
280+
281+
cache.MapUnitToAbbreviation(HowMuchUnit.Some, "soome");
282+
cache.MapUnitToAbbreviation(HowMuchUnit.Some, "soome");
283+
cache.MapUnitToAbbreviation(HowMuchUnit.Some, "soome");
284+
285+
Assert.Equal("soome", cache.GetDefaultAbbreviation(HowMuchUnit.Some));
286+
Assert.Equal(new[] { "soome" }, cache.GetUnitAbbreviations(HowMuchUnit.Some));
287+
Assert.Equal(new[] { "soome" }, cache.GetAllUnitAbbreviationsForQuantity(typeof(HowMuchUnit)));
288+
}
289+
290+
[Fact]
291+
public void MapUnitToDefaultAbbreviation_Twice_SetsNewDefaultAndKeepsOrderOfExistingAbbreviations()
292+
{
293+
var cache = new UnitAbbreviationsCache();
294+
295+
cache.MapUnitToAbbreviation(HowMuchUnit.Some, "soome");
296+
cache.MapUnitToDefaultAbbreviation(HowMuchUnit.Some, "1st default");
297+
cache.MapUnitToDefaultAbbreviation(HowMuchUnit.Some, "2nd default");
298+
299+
Assert.Equal("2nd default", cache.GetDefaultAbbreviation(HowMuchUnit.Some));
300+
Assert.Equal(new[] { "2nd default", "1st default", "soome" }, cache.GetUnitAbbreviations(HowMuchUnit.Some));
301+
}
302+
277303
/// <summary>
278304
/// Convenience method to the proper culture parameter type.
279305
/// </summary>

UnitsNet/CustomCode/UnitAbbreviationsCache.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ static UnitAbbreviationsCache()
5454

5555
private void LoadGeneratedAbbreviations()
5656
{
57-
foreach(var quantity in Quantity.GetQuantityTypes())
57+
foreach (Type quantity in Quantity.GetQuantityTypes())
5858
{
5959
var mapGeneratedLocalizationsMethod = quantity.GetMethod(nameof(Length.MapGeneratedLocalizations), BindingFlags.NonPublic | BindingFlags.Static);
6060
mapGeneratedLocalizationsMethod?.Invoke(null, new object[]{this});
@@ -272,7 +272,7 @@ public string[] GetUnitAbbreviations(Type unitType, int unitValue, IFormatProvid
272272
/// <param name="unitEnumType">Enum type for unit.</param>
273273
/// <param name="formatProvider">The format provider to use for lookup. Defaults to <see cref="CultureInfo.CurrentCulture" /> if null.</param>
274274
/// <returns>Unit abbreviations associated with unit.</returns>
275-
public string[] GetAllUnitAbbreviationsForQuantity(Type unitEnumType, IFormatProvider? formatProvider = null)
275+
public IReadOnlyList<string> GetAllUnitAbbreviationsForQuantity(Type unitEnumType, IFormatProvider? formatProvider = null)
276276
{
277277
formatProvider ??= CultureInfo.CurrentCulture;
278278

@@ -292,7 +292,7 @@ internal bool TryGetUnitValueAbbreviationLookup(Type unitType, IFormatProvider?
292292

293293
formatProvider ??= CultureInfo.CurrentCulture;
294294

295-
if (!_lookupsForCulture.TryGetValue(formatProvider, out var quantitiesForProvider))
295+
if (!_lookupsForCulture.TryGetValue(formatProvider, out UnitTypeToLookup? quantitiesForProvider))
296296
{
297297
return !Equals(formatProvider, FallbackCulture) &&
298298
TryGetUnitValueAbbreviationLookup(unitType, FallbackCulture, out unitToAbbreviations);

UnitsNet/CustomCode/UnitValueAbbreviationLookup.cs

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,93 @@
22
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.Linq;
78

8-
using UnitToAbbreviationMap = System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<string>>;
9-
using AbbreviationToUnitMap = System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<int>>;
10-
119
namespace UnitsNet
1210
{
11+
internal class UnitToAbbreviationMap : ConcurrentDictionary<int, IReadOnlyList<string>> { }
12+
internal class AbbreviationToUnitMap : ConcurrentDictionary<string, IReadOnlyList<int>> { }
13+
1314
internal class UnitValueAbbreviationLookup
1415
{
1516
private readonly UnitToAbbreviationMap _unitToAbbreviationMap = new();
1617
private readonly AbbreviationToUnitMap _abbreviationToUnitMap = new();
1718
private readonly AbbreviationToUnitMap _lowerCaseAbbreviationToUnitMap = new();
19+
private Lazy<string[]> _allUnitAbbreviationsLazy;
20+
private readonly object _syncRoot = new();
1821

19-
internal string[] GetAllUnitAbbreviationsForQuantity()
22+
public UnitValueAbbreviationLookup()
2023
{
21-
return _unitToAbbreviationMap.Values.SelectMany(abbreviations => abbreviations).Distinct().ToArray();
24+
_allUnitAbbreviationsLazy = new Lazy<string[]>(ComputeAllUnitAbbreviationsValue);
25+
}
26+
27+
internal IReadOnlyList<string> GetAllUnitAbbreviationsForQuantity()
28+
{
29+
return _allUnitAbbreviationsLazy.Value;
2230
}
2331

24-
internal List<string> GetAbbreviationsForUnit<TUnitType>(TUnitType unit) where TUnitType : Enum
32+
internal IReadOnlyList<string> GetAbbreviationsForUnit<TUnitType>(TUnitType unit) where TUnitType : Enum
2533
{
2634
return GetAbbreviationsForUnit(Convert.ToInt32(unit));
2735
}
2836

29-
internal List<string> GetAbbreviationsForUnit(int unit)
37+
internal IReadOnlyList<string> GetAbbreviationsForUnit(int unit)
3038
{
3139
if (!_unitToAbbreviationMap.TryGetValue(unit, out var abbreviations))
3240
return new List<string>(0);
3341

34-
return abbreviations.Distinct().ToList();
42+
return abbreviations.ToList();
3543
}
3644

37-
internal List<int> GetUnitsForAbbreviation(string abbreviation, bool ignoreCase)
45+
internal IReadOnlyList<int> GetUnitsForAbbreviation(string abbreviation, bool ignoreCase)
3846
{
3947
var lowerCaseAbbreviation = abbreviation.ToLower();
4048
var key = ignoreCase ? lowerCaseAbbreviation : abbreviation;
4149
var map = ignoreCase ? _lowerCaseAbbreviationToUnitMap : _abbreviationToUnitMap;
4250

43-
if (!map.TryGetValue(key, out List<int> units))
51+
if (!map.TryGetValue(key, out IReadOnlyList<int> units))
4452
return new List<int>(0);
4553

46-
return units.Distinct().ToList();
54+
return units.ToList();
4755
}
4856

4957
internal void Add(int unit, string abbreviation, bool setAsDefault = false, bool allowAbbreviationLookup = true)
5058
{
51-
var lowerCaseAbbreviation = abbreviation.ToLower();
52-
53-
if (!_unitToAbbreviationMap.TryGetValue(unit, out var abbreviationsForUnit))
54-
abbreviationsForUnit = _unitToAbbreviationMap[unit] = new List<string>();
55-
56-
if (allowAbbreviationLookup)
59+
// Restrict concurrency on writes.
60+
// By using ConcurrencyDictionary and immutable IReadOnlyList instances, we don't need to lock on reads.
61+
lock (_syncRoot)
5762
{
58-
if (!_abbreviationToUnitMap.TryGetValue(abbreviation, out var unitsForAbbreviation))
59-
_abbreviationToUnitMap[abbreviation] = unitsForAbbreviation = new List<int>();
60-
61-
if (!_lowerCaseAbbreviationToUnitMap.TryGetValue(lowerCaseAbbreviation, out var unitsForLowerCaseAbbreviation))
62-
_lowerCaseAbbreviationToUnitMap[lowerCaseAbbreviation] = unitsForLowerCaseAbbreviation = new List<int>();
63-
64-
unitsForLowerCaseAbbreviation.Remove(unit);
65-
unitsForLowerCaseAbbreviation.Add(unit);
66-
67-
unitsForAbbreviation.Remove(unit);
68-
unitsForAbbreviation.Add(unit);
63+
var lowerCaseAbbreviation = abbreviation.ToLower();
64+
65+
if (allowAbbreviationLookup)
66+
{
67+
_abbreviationToUnitMap.AddOrUpdate(abbreviation,
68+
addValueFactory: _ => new List<int> { unit },
69+
updateValueFactory: (_, existing) => existing.Append(unit).Distinct().ToList());
70+
71+
_lowerCaseAbbreviationToUnitMap.AddOrUpdate(lowerCaseAbbreviation,
72+
addValueFactory: _ => new List<int> { unit },
73+
updateValueFactory: (_, existing) => existing.Append(unit).Distinct().ToList());
74+
}
75+
76+
_unitToAbbreviationMap.AddOrUpdate(unit,
77+
addValueFactory: _ => new List<string> { abbreviation },
78+
updateValueFactory: (_, existing) =>
79+
{
80+
return setAsDefault
81+
? existing.Where(x => x != abbreviation).Prepend(abbreviation).ToList()
82+
: existing.Where(x => x != abbreviation).Append(abbreviation).ToList();
83+
});
84+
85+
_allUnitAbbreviationsLazy = new Lazy<string[]>(ComputeAllUnitAbbreviationsValue);
6986
}
87+
}
7088

71-
abbreviationsForUnit.Remove(abbreviation);
72-
73-
if (setAsDefault)
74-
abbreviationsForUnit.Insert(0, abbreviation);
75-
else
76-
abbreviationsForUnit.Add(abbreviation);
89+
private string[] ComputeAllUnitAbbreviationsValue()
90+
{
91+
return _unitToAbbreviationMap.Values.SelectMany(abbreviations => abbreviations).Distinct().ToArray();
7792
}
7893
}
7994
}

0 commit comments

Comments
 (0)