在堆栈溢出上,我发现记忆单参数函数的代码:
static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
var d = new Dictionary<A, R>();
return a=>
{
R r;
if (!d.TryGetValue(a, out r))
{
r = f(a);
d.Add(a, r);
}
return r;
};
}
虽然这段代码为我完成了它的工作,但当同时从多个线程调用记忆函数时,它有时会失败:使用相同的参数调用
Add
方法两次并抛出异常。
如何使记忆线程安全?
ConcurrentDictionary.GetOrAdd
它可以满足您所需的一切:
static Func<A, R> ThreadsafeMemoize<A, R>(this Func<A, R> f)
{
var cache = new ConcurrentDictionary<A, R>();
return argument => cache.GetOrAdd(argument, f);
}
函数
f
本身应该是线程安全的,因为它可以同时从多个线程调用。
此代码也不保证每个唯一参数值仅调用函数
f
一次。事实上,在繁忙的环境中,它可以被调用很多次。如果您需要这种合约,您应该查看这个相关问题中的答案,但请注意它们并不那么紧凑并且需要使用锁。
扩展GMan的答案,我想记住一个具有多个参数的函数。以下是我的做法,使用 C#
Tuple
(需要 C# 7)作为 ConcurrentDictionary
的关键。
这种技术可以轻松扩展以允许更多参数:
public static class FunctionExtensions
{
// Function with 1 argument
public static Func<TArgument, TResult> Memoize<TArgument, TResult>
(
this Func<TArgument, TResult> func
)
{
var cache = new ConcurrentDictionary<TArgument, TResult>();
return argument => cache.GetOrAdd(argument, func);
}
// Function with 2 arguments
public static Func<TArgument1, TArgument2, TResult> Memoize<TArgument1, TArgument2, TResult>
(
this Func<TArgument1, TArgument2, TResult> func
)
{
var cache = new ConcurrentDictionary<(TArgument1, TArgument2), TResult>();
return (argument1, argument2) =>
cache.GetOrAdd((argument1, argument2), tuple => func(tuple.Item1, tuple.Item2));
}
}
例如:
Func<int, string> example1Func = i => i.ToString();
var example1Memoized = example1Func.Memoize();
var example1Result = example1Memoized(66);
Func<int, int, int> example2Func = (a, b) => a + b;
var example2Memoized = example2Func.Memoize();
var example2Result = example2Memoized(3, 4);
(当然,为了获得记忆的好处,您通常希望将
example1Memoized
/ example2Memoized
保留在类变量中或它们不是短暂存在的地方)。
就像 Gman 提到的那样,
ConcurrentDictionary
是执行此操作的首选方法,但是如果这不适用于简单的 lock
语句就足够了。
static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
var d = new Dictionary<A, R>();
return a=>
{
R r;
lock(d)
{
if (!d.TryGetValue(a, out r))
{
r = f(a);
d.Add(a, r);
}
}
return r;
};
}
使用锁而不是
ConcurrentDictionary
的一个潜在问题是这种方法可能会在程序中引入死锁。
_memo1 = Func1.Memoize()
和_memo2 = Func2.Memoize()
,其中_memo1
和_memo2
是实例变量。_memo1
,Func1
开始处理。_memo2
,在 Func2
内部有对 _memo1
的调用,Thread2 会阻塞。Func1
的处理在函数后期调用 _memo2
,Thread1 会阻塞。因此,如果可能的话,请使用
ConcurrentDictionary
,但如果不能并且使用锁,则不要在 Memoized 函数内部或您向自己开放时调用范围在您正在运行的函数之外的其他 Memoized 函数死锁的风险(如果 _memo1
和 _memo2
是局部变量而不是实例变量,则不会发生死锁)。
(请注意,使用
ReaderWriterLock
可能会稍微提高性能,但您仍然会遇到相同的死锁问题。)
使用 System.Collections.Generic;
Dictionary<string, string> _description = new Dictionary<string, string>();
public float getDescription(string value)
{
string lookup;
if (_description.TryGetValue (id, out lookup)) {
return lookup;
}
_description[id] = value;
return lookup;
}
如果您想指定如何为参数集构建密钥,除了使用 ConcurrentDictionary 之外,还可以指定 keyGenerator 函数。我这里使用 TryAdd 方法而不是 GetOrUpdate,期望字典中缺少的项只添加一次,稍后返回缓存结果,无需更新。
使用示例: 假设 T1 是带有属性密钥 ID 的书籍,T2 是带有属性名称的版本。
public static Func<T1, TOut> MemoizeV2<T1, TOut>(this Func<T1, TOut> @this, Func<T1, string> keyGenerator)
{
var dict = new ConcurrentDictionary<string, TOut>();
return x =>
{
string key = keyGenerator(x);
if (!dict.ContainsKey(key))
{
dict.TryAdd(key, @this(x));
}
return dict[key];
};
}
public static Func<T1, T2, TOut> MemoizeV2<T1, T2, TOut>(this Func<T1, T2, TOut> @this, Func<T1, T2, string> keyGenerator)
{
var dict = new ConcurrentDictionary<string, TOut>();
return (x, y) =>
{
string key = keyGenerator(x, y);
if (!dict.ContainsKey(key))
{
dict.TryAdd(key, @this(x, y));
}
return dict[key];
};
}
public static Func<T1, T2, T3, TOut> MemoizeV2<T1, T2, T3, TOut>(this Func<T1, T2, T3, TOut> @this, Func<T1, T2, T3, string> keyGenerator)
{
var dict = new ConcurrentDictionary<string, TOut>();
return (x, y, z) =>
{
string key = keyGenerator(x, y, z);
if (!dict.ContainsKey(key))
{
dict.TryAdd(key, @this(x, y, z));
}
return dict[key];
};
}
public static Func<T1, T2, T3, T4, TOut> MemoizeV2<T1, T2, T3, T4, TOut>(this Func<T1, T2, T3, T4, TOut> @this, Func<T1, T2, T3, T4, string> keyGenerator)
{
var dict = new ConcurrentDictionary<string, TOut>();
return (x, y, z, w) =>
{
string key = keyGenerator(x, y, z, w);
if (!dict.ContainsKey(key))
{
dict.TryAdd(key, @this(x, y, z, w));
}
return dict[key];
};
}
使用,考虑我们想要用一些示例数据来记忆的方法:
public string GetActorsByMovieTitleAndNumActors(string movieTitle, int numberOfActors)
{
Console.WriteLine($"Retrieving actors for movie with title {movieTitle} number of actors: {numberOfActors} at: {DateTime.Now} ");
List<Movie> movies1997 = System.Text.Json.JsonSerializer.Deserialize<List<Movie>>(movies1997json);
string actors = string.Join(",", movies1997.FirstOrDefault(m => m.name?.ToLower() == movieTitle?.ToLower())?.actors.Take(numberOfActors).ToArray());
return actors;
}
下面的代码显示了如何为输入参数集指定复合键,在本例中为两个。我们只是连接第一个和第二个参数。
var GetActorsByMovieTitleNumActors = ((string movieTitle, int numberOfActors) => movieStore.GetActorsByMovieTitleAndNumActors(movieTitle, numberOfActors));
var GetActorsByMovieNumActorsM = GetActorsByMovieTitleNumActors.MemoizeV2((x,y) => x + y);
var starShipTroopersActorsV2 = GetActorsByMovieTitleNumActors("Starship troopers", 2);
starShipTroopersActorsV2.Dump("Starship troopers - Call to method #1 time");