Подтвердить что ты не робот

Меморандум по потоку

Возьмем подход Wes Dyer к функции memoization в качестве отправной точки:

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

Любые лучшие варианты?

4b9b3361

Ответ 1

Используйте .net 4.0 ConcurrentDictionary<A, R> без ненужного Lazy<R>.
Ключ GetOrAdd(A, Func<A, R>), который превращается в красиво тривиальную лямбду.

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 элементов показывают 19 мс для ConcurrentDictionary - то же самое, что и обычный 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;
    };
}

Ответ 2

Если у вас уже есть тип 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;
    };
}

Ответ 3

Ответ Томаса, похоже, не компилируется в .NET 4.0 из-за параметра enum для Lazy-конструктора. Я пересмотрел его ниже. Я также добавил необязательный параметр для подачи одного собственного анализатора равенства. Это полезно, если 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))));
    }

Кажется, что он работает правильно.

Ответ 4

Нет, это не лучшие варианты.

Версия с ленивой оценкой бессмысленна, так как вы все равно ее оцениваете. Версия со словарем синхронизации не работает должным образом, поскольку вы не защищаете словарь карт внутри замка перед его использованием.

Версия, которую вы назвали ужасной, на самом деле является лучшим вариантом. Вы должны защищать словарь карт внутри блокировки, чтобы к нему мог обращаться только один поток за раз. Словарь не является потокобезопасным, поэтому, если вы позволяете одному потоку читать его, а другой поток меняет его, у вас будут проблемы.

Помните, что использование блокировки на объекте карты не защищает объект карты сам по себе, он использует только ссылку на карту в качестве идентификатора, чтобы хранить более одного потока за раз, чтобы запустить код внутри блокировки. Вы должны поместить весь код, который обращается к объекту внутри блокировки, а не только к коду, который меняет объект.

Ответ 5

Вы не хотите вычислять одно и то же значение дважды, и вы хотите, чтобы многие потоки могли одновременно вычислять значения и извлекать значения. Для этого вам нужно будет использовать какую-то переменную состояния и мелкозернистую систему блокировки.

Вот эта идея. когда значения нет, вы помещаете значение в карту синхронизации, а затем любой поток, которому это значение будет ждать, в противном случае вы просто возьмете текущее значение. таким образом, блокировка карты минимизируется для запроса значений и возвращаемых значений.

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

        };
    }

Это просто первая попытка, но я думаю, что это демонстрирует технику. Здесь вы торгуете дополнительной памятью для скорости.

Ответ 6

Вы прочитали комментарий от Dyer, связанный с потокобезопасностью в статье?

Вероятно, самый простой способ сделать Memoize потокобезопасным - поместить замок на карту.

Это гарантирует, что функция, которая будет memoized, будет запускаться только один раз для каждого набора различных аргументов.

В моем примере игры RoboRally я фактически использовал функцию memoization, чтобы действовать как "суррогатный синглтон". Это не очень синглтон, так как может быть один экземпляр для экземпляра factory (если только factory не является статичным). Но это именно то, что я хотел.

Ответ 7

Развернувшись на превосходном ответе Найджела Тон, я хотел предложить повторно используемый компонент, извлеченный из его решения, ограничивающий подсчет вызовов для 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);
}

Ура!