如何在c#中执行线程安全的函数memoization?

时间:2013-12-12 13:18:28

标签: c# .net multithreading memoization

这里堆栈溢出我found记住单参数函数的代码:

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;
    };
}

虽然这段代码对我来说很有用,但是当同时从多个线程调用memoized函数时,它会失败:Add方法被相同的参数调用两次并抛出异常。

如何使memoization线程安全?

4 个答案:

答案 0 :(得分:22)

您可以使用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。事实上,在繁忙的环境中可以多次调用它。如果你需要这种合同,你应该看看这个related question中的答案,但要注意它们不是那么紧凑并且需要使用锁。

答案 1 :(得分:2)

就像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的一个潜在问题是此方法可能会在程序中引入死锁。

  1. 您有两个记忆函数_memo1 = Func1.Memoize()_memo2 = Func2.Memoize(),其中_memo1_memo2是实例变量。
  2. Thread1调用_memo1Func1开始处理。
  3. Thread2调用_memo2,在Func2内调用_memo1和Thread2阻止。
  4. Thread1对Func1的处理在函数后期调用_memo2,Thread1阻塞。
  5. DEADLOCK!
  6. 因此,如果可能的话,使用ConcurrentDictionary,但是如果你不能并且你使用锁而不是在内置Memoized函数时调用其他在你运行的函数之外的其他Memoized函数或者你让自己面临死锁的风险(如果_memo1_memo2是局部变量而不是实例变量,则不会发生死锁。)

    (注意,使用ReaderWriterLock可能会略微改善性能,但您仍会遇到相同的死锁问题。)

答案 2 :(得分:2)

GMan's answer上扩展,我想记住一个带有多个参数的函数。这是我的操作方式,使用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保留在类变量中或它们并非短暂的地方)。

答案 3 :(得分:0)

使用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;
}