26

让我们以Wes Dyer 的函数记忆方法为起点:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, R>();
  return a =>
    {
      R value;
      if (map.TryGetValue(a, out value))
        return value;
      value = f(a);
      map.Add(a, value);
      return value;
    };
}

问题是,当从多个线程中使用它时,我们可能会遇到麻烦:

Func<int, int> f = ...
var f1 = f.Memoize();
...
in thread 1:
var y1 = f1(1);
in thread 2:
var y2 = f1(1);
// We may be recalculating f(1) here!

让我们尽量避免这种情况。锁定map

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, R>();
  return a =>
    {
      R value;
      lock(map) 
      {
        if (map.TryGetValue(a, out value))
          return value;
        value = f(a);
        map.Add(a, value);
      }
        return value;
    };
}

显然是一个可怕的想法,因为它阻止我们同时计算f1许多不同的论点。如果具有值类型,则锁定a将不起作用a(无论如何都是一个坏主意,因为我们无法控制a并且外部代码也可能锁定它)。

以下是我能想到的两个选择:

假设一个Lazy<T>懒惰评估类(见这里):

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, Lazy<R>>();
  return a =>
    {
      Lazy<R> result;
      lock(map) 
      {
        if (!map.TryGetValue(a, out result))
        {  
          result = () => f(a);
          map.Add(a, result);
        }
      }
      return result.Value;
    };
}

或者保留一个额外的对象字典以进行同步:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new Dictionary<A, R>();
  var mapSync = new Dictionary<A, object>();
  return a =>
    {
      R value;
      object sync;
      lock(mapSync)
      { 
        if (!mapSync.TryGetValue(a, out sync))
        { 
          sync = new object();
          mapSync[a] = sync;
        }
      }
      lock(map)
      {
        if (map.TryGetValue(a, out value))
          return value;
      }
      lock(sync)
      {
        value = f(a);
        lock(map)
        {
          map[a] = value;
        }
        return value;
      }
    };
}

有更好的选择吗?

4

7 回答 7

44

使用 .net 4.0ConcurrentDictionary<A, R>没有不必要的Lazy<R>.
关键是GetOrAdd(A, Func<A, R>)渲染成一个非常简单的 lambda。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, R>();
    return a => cache.GetOrAdd(a, f);
};

更新上述解决方案确实允许多个同时读取器和写入器以最小的开销。但是,它不会阻止f(a)对相同的值执行多次(在计算期间)。

如果这对您很重要,您可以将值包含在内,Lazy<R>但每次读取都会产生成本。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, Lazy<R>>();
    return a => cache.GetOrAdd(a, new Lazy<R>(() => f(a))).Value;
}

对预填充的 1000 项缓存进行一百万次读取的更新时序测试显示,与常规ConcurrentDictionary相同,为19 毫秒,Dictionary但版本为720 毫秒Lazy

如果这听起来太陡峭,您可以通过更复杂的解决方案获得两全其美的效果。

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, R>();
    var syncMap = new ConcurrentDictionary<A, object>();
    return a =>
    {
        R r;
        if (!cache.TryGetValue(a, out r))
        {
            var sync = syncMap.GetOrAdd(a, new object());
            lock (sync)
            {
                r = cache.GetOrAdd(a, f);
            }
            syncMap.TryRemove(a, out sync);
        }
        return r;
    };
}
于 2012-02-16T23:03:22.193 回答
10

如果您已经拥有该Lazy<T>类型,我假设您使用的是 .net 4.0,因此您也可以使用ConcurrentDictionary<A,R>

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
  var map = new ConcurrentDictionary<A, Lazy<R>>();
  return a =>
    {
      Lazy<R> lazy = new Lazy<R>(() => f(a), LazyExecutionMode.EnsureSingleThreadSafeExecution);
      if(!map.TryAdd(a, lazy))
      {
        return map[a].Value;
      }
      return lazy.Value;
    };
}
于 2009-08-10T14:10:59.587 回答
3

扩展 Nigel Touch 的出色答案,我想提供一个从他的解决方案中提取的可重用组件,以限制 f(a) 的调用次数。

我称之为 SynchronizedConcurrentDictionary,它看起来像这样:

public class SynchronizedConcurrentDictionary<TKey, TValue> : ConcurrentDictionary<TKey, TValue>
{
    private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();

    public new TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
    {
        TValue result;

        _cacheLock.EnterWriteLock();
        try
        {
            result = base.GetOrAdd(key, valueFactory);
        }
        finally
        {
            _cacheLock.ExitWriteLock();
        }

        return result;
    }
}

那么 Memoize 函数就变成了两行:

public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var cache = new SynchronizedConcurrentDictionary<A, R>();

    return key => cache.GetOrAdd(key, f);
}

干杯!

于 2015-06-11T19:05:28.613 回答
2

由于 Lazy 构造函数的 enum 参数,Thomas 的答案似乎无法在 .NET 4.0 下编译。我在下面对其进行了修改。我还添加了一个可选参数,用于提供自己的相等比较器。例如,如果 TInput 没有实现它自己的 Equals 或者如果 TInput 是一个字符串并且您想让它不区分大小写,这将很有用。

    public static Func<TInput, TResult> Memoize<TInput, TResult>(
        this Func<TInput, TResult> func, IEqualityComparer<TInput> comparer = null)
    {
        var map = comparer == null
                      ? new ConcurrentDictionary<TInput, Lazy<TResult>>()
                      : new ConcurrentDictionary<TInput, Lazy<TResult>>(comparer);

        return input =>
               {
                   var lazy = new Lazy<TResult>(() => func(input), LazyThreadSafetyMode.ExecutionAndPublication);

                   return map.TryAdd(input, lazy)
                              ? lazy.Value
                              : map[input].Value;
               };
    }

我用这个作为我的测试对这个方法做了一些基本的测试:

    public void TestMemoize()
    {
        Func<int, string> mainFunc = i =>
                                     {
                                         Console.WriteLine("Evaluating " + i);
                                         Thread.Sleep(1000);
                                         return i.ToString();
                                     };

        var memoized = mainFunc.Memoize();

        Parallel.ForEach(
            Enumerable.Range(0, 10),
            i => Parallel.ForEach(Enumerable.Range(0, 10), j => Console.WriteLine(memoized(i))));
    }

它似乎工作正常。

于 2010-12-13T18:11:56.243 回答
1

不,它们不是更好的选择。

具有惰性评估的版本毫无意义,因为无论如何您都会立即对其进行评估。具有同步字典的版本无法正常工作,因为您在使用它之前没有保护锁内的地图字典。

你所说的可怕的版本实际上是最好的选择。您必须在锁内保护地图字典,以便一次只有一个线程可以访问它。字典不是线程安全的,所以如果你让一个线程读取它而另一个线程正在更改它,你就会遇到问题。

请记住,在地图对象上使用锁并不能保护地图对象本身,它只是使用地图引用作为标识符来一次保留多个线程来运行锁内的代码。您必须将访问对象的所有代码放入锁中,而不仅仅是更改对象的代码。

于 2009-08-10T14:07:15.613 回答
1

您不希望两次计算相同的值,并且希望多个线程能够同时计算值和/或检索值。为此,您需要使用某种条件变量和细粒度的锁定系统。

继承人的想法。当没有值存在时,您将一个值放入同步映射中,然后任何需要该值的线程将等待它,否则您将只获取当前值。通过这种方式,地图的锁定被最小化为查询值和返回值。

    public static Func<A, R> Memoize<A, R>(this Func<A, R> f)
    {
        var map = new Dictionary<A, R>();
        var mapSync = new Dictionary<A, object>();
        return a =>
        {
            R value;
            object sync = null;
            bool calc = false;
            bool wait = false;
            lock (map)
            {
                if (!map.TryGetValue(a, out value))
                {
                    //its not in the map
                    if (!mapSync.TryGetValue(a, out sync))
                    {
                        //not currently being created
                        sync = new object();
                        mapSync[a] = sync;
                        calc = true;

                    }
                    else
                    {
                        calc = false;
                        wait = true;
                    }
                }
            }
            if(calc)
            {
                lock (sync)
                {
                    value = f(a);
                    lock (map)
                    {
                        map.Add(a, value);
                        mapSync.Remove(a);
                    }
                    Monitor.PulseAll(sync);
                    return value;
                }
            }
            else if (wait)
            {
                lock (sync)
                {
                    while (!map.TryGetValue(a, out value))
                    {
                        Monitor.Wait(sync);
                    }
                    return value;
                }
            }

            lock (map)
            {
                return map[a];
            }

        };
    }

这只是一个快速的第一次尝试,但我认为它展示了这项技术。在这里,您以额外的内存换取速度。

于 2009-08-10T15:07:20.480 回答
0

您是否阅读了Dyer在文章中与线程安全相关的评论?

可能使 Memoize 线程安全的最简单方法是在 map 上加锁。

这将确保被记忆的函数只会为每组不同的参数运行一次。

在我的 RoboRally 游戏示例中,我实际上使用函数记忆来充当“代理单例”。它并不是真正的单例,因为每个工厂实例可以有一个实例(除非工厂是静态的)。但这正是我想要的。

于 2009-08-10T14:20:10.073 回答