Как избежать асинхронного ожидания, если я знаю, что большую часть времени результат будет кэширован - PullRequest
0 голосов
/ 18 февраля 2019

Какой из них является лучшим вариантом при работе с кэшированием в C #?

Меня интересует на уровне компилятора, какое из них является наиболее элегантным / производительным решением.

Например, использует ли компилятор .net какие-либо хитрости, чтобы знать, что когда код будет работать синхронно и избегать создания / запуска ненужного асинхронного кода ожидания?

Вариант 1, Использовать async/await и использовать Task.FromResult для кэшированных значений;

        public async Task<T> GetValue<T>(string key)
        {
            if (_cache.containsKey(key))
            {
                // 99% of the time will hit this
                return Task.FromResult(_cache.GetItem(key));
            }

            return  await _api.GetValue(key);

        }

Вариант 2, избегайте async/await и используйте что-то вроде GetAwaiter().GetResult() в течение нескольких раз, когда будет достигнута конечная точка API.

        public  T GetValue<T>(string key)
        {
            if (_cache.containsKey(key))
            {
                // 99% of the time will hit this
                return _cache.GetItem(key);
            }

            return _api.GetValue(key).GetAwaiter().GetResult();

        }

Любые идеибудет очень признателен.

Ответы [ 4 ]

0 голосов
/ 18 февраля 2019

Вероятно, вы ищете памятка .

Реализация может выглядеть примерно так:

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

Measure(() => slowSquare(2));   // 00:00:00.1009680
Measure(() => slowSquare(2));   // 00:00:00.1006473
Measure(() => slowSquare(2));   // 00:00:00.1006373
var memoizedSlow = slowSquare.Memoize();
Measure(() => memoizedSlow(2)); // 00:00:00.1070149
Measure(() => memoizedSlow(2)); // 00:00:00.0005227
Measure(() => memoizedSlow(2)); // 00:00:00.0004159

Источник

0 голосов
/ 18 февраля 2019

Прежде всего, это требует связывания скорости разгона:

https://ericlippert.com/2012/12/17/performance-rant/

Микрооптимизации, подобные этим, обычно оставляются JiT.Мое эмпирическое правило таково: если вам действительно нужно это различие, то вы, вероятно, имеете дело с программированием в реальном времени.А для Realtime Proramming the Garbage Collected runtime, как .NET, вероятно, была неправильная среда для начала.Что-то с прямым управлением памятью, такое как небезопасный код - даже родной C ++ или Assembler - было бы лучше.

Во-вторых, задача может быть просто неправильным инструментом.Может быть, вы действительно хотите что-то вроде Lazy [T] ?Или какой-нибудь из 5 разных классов Чаша?(как и в случае с таймером, существует около одного для конкретной технологии пользовательского интерфейса).

Можно использовать любой инструмент для многих целей.Но задачи для многозадачности и есть лучшие инструменты для кэширования и отложенной инициализации.И Lazy [T] даже неотъемлемо сохраняет Thread.

0 голосов
/ 18 февраля 2019

Ваш первый не сработает.Самое простое и наиболее подходящее из них:

public async Task<T> GetValueAsync<T>(string key)
{
  if (_cache.ContainsKey(key))
  {
    return _cache.GetItem(key);
  }

  T result = await _api.GetValueAysnc(key);
  _cache.Add(key, result);
  return result;
}

Или еще лучше, если это возможно:

public async Task<T> GetValueAsync<T>(string key)
{
  if (_cache.TryGet(key, out T result))
  {
    return result;
  }

  result = await _api.GetValueAysnc(key);
  _cache.Add(key, result);
  return result;
}

Это отлично работает и вернет уже выполненное задание.если значение находилось в кеше, то await будет немедленно продолжено.

Однако , если значение находится в кеше большую часть времени и метод вызывается достаточно часто, чтобы дополнительное устройство около async имело значение, тогда его можно полностью избежать в таком случае:

public Task<T> GetValueAsync<T>(string key)
{
  if (_cache.TryGet(key, out Task<T> result))
  {
    return result;
  }

  return GetAndCacheValueAsync(string key);
}

private async Task<T> GetAndCacheValueAsync<T>(string key)
{
  var task = _api.GetValueAysnc(key);
  result = await task;
  _cache.Add(key, task);
  return result;
}

Здесь, если значение кэшируется, мы избегаем как конечного автоматавокруг async, а также создание нового Task<T>, поскольку мы сохранили фактическое Task.Каждый из них выполняется только в первом случае.

0 голосов
/ 18 февраля 2019

Официальный подход заключается в кэшировании Task<T>, а не T.

. Это также имеет то преимущество, что если кто-то запрашивает значение, вы можете запустить запрос для извлечения значения изатем кешируем полученное в процессе Task<T>.Если кто-то еще запрашивает кэшированное значение до того, как запрос будет завершен, ему также будет выдан тот же незавершенный процесс Task<T>, и вы не завершите выполнение двух запросов.

Например:

public Task<T> GetValue<T>(string key)
{
    // Prefer a TryGet pattern if you can, to halve the number of lookups
    if (_cache.containsKey(key))
    {
        return _cache.GetItem(key);
    }

    var task = _api.GetValue(key);
    _cache.Add(key, task);
    return task;
}

Обратите внимание, что в этом случае вам нужно подумать о сбое: если запрос к API завершится неудачно, вы будете кэшировать Task, который содержит исключение.Это может быть то, что вы хотите, но это может быть не так.

Если по какой-то причине вы не можете этого сделать, то официальный совет должен использовать ValueTask<T> для высокопроизводительных сценариев.У этого типа есть некоторые ошибки (например, вы не можете ждать его дважды), поэтому я рекомендую прочитать это .Если у вас нет высоких требований к производительности, Task.FromResult хорошо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...