Каковы передовые методы/хорошие шаблоны для управления кэшированными асинхронными данными?

avatar
stenci
1 июля 2021 в 16:01
132
1
-1

Я переписываю старое приложение и пытаюсь использовать async для его ускорения.

Старый код делал что-то вроде этого:

var value1 = getValue("key1");
var value2 = getValue("key2");
var value3 = getValue("key3");

где функция getValue управляла собственным кешем в словаре, делая что-то вроде этого:

object getValue(string key) {
  if (cache.ContainsKey(key)) return cache[key];
  var value = callSomeHttpEndPointsAndCalculateTheValue(key);
  cache.Add(key, value);
  return value;
}

Если я делаю getValue async и await каждый вызов getValue, то все работает хорошо. Но она не быстрее старой версии, потому что все работает синхронно, как раньше.

Если я удалю await (ну, если я отложу это, но это не основное в этом вопросе), я, наконец, заставлю медленные вещи работать параллельно. Но если второй вызов getValue("key1") выполняется до завершения первого вызова, я в конечном итоге выполняю один и тот же медленный вызов дважды, и все работает медленнее, чем в старой версии, потому что она не использует кэш.<

. >

Есть ли что-то вроде await("key1"), которое будет ожидать, только если предыдущий вызов с "key1" все еще ожидает?

EDIT (продолжение комментария)

Под "ускорить" я имею в виду более отзывчивый.

Например, когда пользователь выбирает материал в раскрывающемся списке, я хочу обновить список доступных толщин или цветов в других раскрывающихся списках и другие свойства материала в других элементах пользовательского интерфейса. Иногда это запускает каскад событий, требующих многократного использования одного и того же getValue("key").

.

Например, при изменении материала могут быть вызваны несколько функций: updateThicknesses(), updateHoleOffsets(), updateMaxWindLoad(), updateMaxHoleDistances() и т. д. Каждая функция считывает значения из элементов пользовательского интерфейса и решает, делать ли собственные медленные вычисления независимо от других функций. Каждой функции может потребоваться несколько вызовов http для вычисления некоторых параметров, а некоторые из этих параметров могут потребоваться нескольким функциям.

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

.

Новая асинхронная реализация вызывает все функции одновременно, поэтому каждая функция в конечном итоге вызывает одни и те же конечные точки http, поскольку их результаты еще не кэшированы.

Источник
Theodor Zoulias
1 июля 2021 в 16:12
1

"Я пытаюсь использовать асинхронный режим для его ускорения" Обычно асинхронный режим/ожидание используется, чтобы сделать приложение более отзывчивым или масштабируемым, а не более быстрым. Можете ли вы показать, как вы собираетесь использовать асинхронную версию метода getValue (getValueAsync?), чтобы ускорить работу приложения?

Alexander Petrov
1 июля 2021 в 16:29
1

Загрузите бесплатную небольшую книгу Стивена Туба Асинхронный шаблон на основе задач. Там есть реализация AsyncCache.

John Wu
1 июля 2021 в 16:33
1

Вас может заинтересовать что-то вроде AsyncLazy.

Lasse V. Karlsen
1 июля 2021 в 16:41
1

Простой метод заключается в кэшировании объектов задач вместо значений из них.

Theodor Zoulias
1 июля 2021 в 16:47
0

Вы можете использовать ConcurrentDictionary<K,V> с реализацией GetOrAddAsync, которая принимает асинхронные делегаты (Func<TKey, Task<TValue>>). Есть несколько реализаций этого метода здесь.

Ответы (1)

avatar
Lasse V. Karlsen
1 июля 2021 в 16:46
2

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

Если несколько параллельных задач попытаются получить значение, используя один и тот же ключ, только первая задача будет выделена, остальные будут ожидать той же задачи.

Вот простая реализация:

private Dictionary<string, Task<object>> cache = new();
public Task<object> getValueAsync(string key)
{
    lock (cache)
    {
        if (!cache.TryGetValue(key, out var result))
            cache[key] = result = callSomeHttpEndPointsAndCalculateTheValueAsync(key);

        return result;
    }
}

Судя по комментариям, следующий пример, вероятно, не следует использовать.

Поскольку упоминался [ConcurrentDictionary](), вот версия, использующая это вместо этого.
private ConcurrentDictionary<string, Task<object>> cache = new();
public Task<object> getValueAsync(string key)
{
    return cache.GetOrAdd(key, k => callSomeHttpEndPointsAndCalculateTheValueAsync(k));
}

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

stenci
1 июля 2021 в 16:51
0

Словарь кеша не обязательно должен быть ConcurrentDictionary, потому что он используется только этой функцией, и нет риска неожиданного доступа/изменения, верно?

Lasse V. Karlsen
1 июля 2021 в 16:52
0

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

stenci
1 июля 2021 в 16:55
0

Я новичок в С# и, как я это понимаю, если переменная cache используется только одной функцией и эта функция не имеет await, то ее можно считать потокобезопасной. Это предположение неверно? (Это должен быть другой пост?)

Servy
1 июля 2021 в 17:15
0

@stenci То, что он вызывается только из одного метода, не является проблемой, важно то, вызывался ли он когда-либо из нескольких потоков одновременно. Один вызывающий метод может вызываться или не вызываться одновременно из нескольких потоков.

Servy
1 июля 2021 в 17:16
0

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