【问题标题】:How to perform thread-safe function memoization in c#?如何在 C# 中执行线程安全的函数记忆?
【发布时间】:2013-12-30 22:21:17
【问题描述】:

在堆栈溢出时,我有 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 方法被调用两次,使用相同的参数并引发异常。

如何使内存化线程安全?

【问题讨论】:

    标签: c# .net multithreading memoization


    【解决方案1】:

    扩展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 保留在类变量中或它们不会短暂存在的地方)。

    【讨论】:

      【解决方案2】:

      您可以使用 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 中的答案,但要注意它们不够紧凑,需要使用锁。

      【讨论】:

      • 请注意,GetOrAdd 不会完全阻止 f 对于给定参数被多次调用;它只保证只有 一个 调用的结果被添加到字典中。如果在添加缓存值之前线程同时检查缓存,您可以获得多个调用。这通常不值得担心,但我会提到它,以防调用有不必要的副作用。
      • @JamesWorld 是的,没错。编辑答案以反映这一点,谢谢!
      • 我有点困惑 - cache 这里不是局部变量吗?每次调用ThreadsafeMemoize(),会不会新建一个字典?
      • @dashnick 是的,但是您提供一个函数并返回一个函数。因此,要记忆每个函数,您只需调用一次ThreadsafeMemoize,然后使用返回的函数代替您放入的函数。这样每个记忆函数都会创建一次缓存。
      • 如果你想记住一个有多个参数的函数,你可能对my answer below感兴趣。
      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        就像 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. 死锁!

        因此,如果可能,请使用ConcurrentDictionary,但如果您不能使用锁,请不要调用其他在您正在运行的函数内部的 Memoized 函数,或者您打开自己达到死锁的风险(如果_memo1_memo2 是局部变量而不是实例变量,则不会发生死锁)。

        (注意,使用ReaderWriterLock 可能会稍微提高性能,但您仍然会遇到同样的死锁问题。)

        【讨论】:

          猜你喜欢
          • 2010-11-18
          • 1970-01-01
          • 1970-01-01
          • 2014-08-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多