使用 ImmutableSortedSet 作为线程安全缓存
本文关键字:线程 安全 缓存 ImmutableSortedSet 使用 | 更新日期: 2023-09-27 18:33:06
我有一个方法,它接受一个DateTime
并返回标记该季度结束的日期。由于涉及工作日和假日日历的一些复杂性,我想缓存结果以加快后续调用的速度。我正在使用SortedSet<DateTime>
来维护数据缓存,并且我使用 GetViewBetween
方法来执行缓存查找,如下所示:
private static SortedSet<DateTime> quarterEndCache = new SortedSet<DateTime>();
public static DateTime GetNextQuarterEndDate(DateTime date)
{
var oneDayLater = date.AddDays(1.0);
var fiveMonthsLater = date.AddMonths(5);
var range = quarterEndCache.GetViewBetween(oneDayLater, fiveMonthsLater);
if (range.Count > 0)
{
return range.Min;
}
// Perform expensive calc here
}
现在我想让我的缓存线程安全。与其在任何地方使用锁,这会导致每次查找的性能下降,我正在探索新的ImmutableSortedSet<T>
集合,这将使我完全避免锁。问题是ImmutableSortedSet<T>
没有方法GetViewBetween
。有没有办法从ImmutableSortedSet<T>
获得类似的功能?
[编辑]
Servy说服我,仅使用带有普通SortedSet<T>
的锁是最简单的解决方案。不过,我将保持这个问题悬而未决,因为我有兴趣知道ImmutableSortedSet<T>
是否可以有效地处理这种情况。
让我们将问题分为两部分:
-
如何获得类似于
GetViewBetween
的功能ImmutableSortedSet<T>
?我建议使用IndexOf
方法。在下面的代码片段中,我创建了一个扩展方法GetRangeBetween
它应该可以完成这项工作。 -
如何使用数据不可变的数据结构实现无锁、线程安全的更新?尽管这不是最初的问题,但对这个问题有一些怀疑的评论。不可变框架实现了一个专门用于此目的的方法:
System.Collections.Immutable.Update<T>(ref T location, Func<T, T> transformer) where T : class;
该方法在内部依赖于原子比较/交换操作。如果你想手动完成此操作,你会在下面找到一个替代实现,它的行为应该与Immutable.Update相同。
所以这是代码:
public static class ImmutableExtensions
{
public static IEnumerable<T> GetRangeBetween<T>(
this ImmutableSortedSet<T> set, T min, T max)
{
int i = set.IndexOf(min);
if (i < 0) i = ~i;
while (i < set.Count)
{
T x = set[i++];
if (set.KeyComparer.Compare(x, min) >= 0 &&
set.KeyComparer.Compare(x, max) <= 0)
{
yield return x;
}
else
{
break;
}
}
}
public static void LockfreeUpdate<T>(ref T item, Func<T, T> fn)
where T: class
{
T x, y;
do
{
x = item;
y = fn(x);
} while (Interlocked.CompareExchange(ref item, y, x) != x);
}
}
用法:
private static volatile ImmutableSortedSet<DateTime> quarterEndCache =
ImmutableSortedSet<DateTime>.Empty;
private static volatile int counter; // test/verification purpose only
public static DateTime GetNextQuarterEndDate(DateTime date)
{
var oneDayLater = date.AddDays(1.0);
var fiveMonthsLater = date.AddMonths(5);
var range = quarterEndCache.GetRangeBetween(oneDayLater, fiveMonthsLater);
if (range.Any())
{
return range.First();
}
// Perform expensive calc here
// -> Meaningless dummy computation for verification purpose only
long x = Interlocked.Increment(ref counter);
DateTime test = DateTime.FromFileTime(x);
ImmutableExtensions.LockfreeUpdate(
ref quarterEndCache,
c => c.Add(test));
return test;
}
[TestMethod]
public void TestIt()
{
var tasks = Enumerable
.Range(0, 100000)
.Select(x => Task.Factory.StartNew(
() => GetNextQuarterEndDate(DateTime.Now)))
.ToArray();
Task.WaitAll(tasks);
Assert.AreEqual(100000, counter);
}