GetOrAdd
をConcurrentDictionary
とともにWebサービスのキャッシュとして使用したいのですが。この辞書の非同期バージョンはありますか? GetOrAddはHttpClient
を使用してWebリクエストを作成するので、GetOrAddが非同期であったバージョンのディクショナリがあったらいいでしょう。
混乱を解消するために、辞書のコンテンツは、Webサービスへの呼び出しからの応答になります。
ConcurrentDictionary<string, Response> _cache = new ConcurrentDictionary<string, Response>();
var response = _cache.GetOrAdd("id", (x) => { _httpClient.GetAsync(x).GetAwaiter().GetResponse();} )
GetOrAdd
は、辞書の値へのアクセスは長時間実行される操作ではないため、非同期操作にはなりません。
ただし、実行できるのは、マテリアライズされた結果ではなく、単にタスクをディクショナリに格納することです。結果が必要な人は、そのタスクを待つことができます。
ただし、操作が複数回ではなく、一度だけ開始されるようにする必要もあります。一部の操作が複数回ではなく1回だけ実行されるようにするには、Lazy
も追加する必要があります。
ConcurrentDictionary<string, Lazy<Task<Response>>> _cache = new ConcurrentDictionary<string, Lazy<Task<Response>>>();
var response = await _cache.GetOrAdd("id", url => new Lazy<Task<Response>>(_httpClient.GetAsync(url))).Value;
GetOrAdd
メソッドは、この目的で使用するのにそれほど適していません。ファクトリーが1回だけ実行されることは保証されないため、正しいバケットを2回ハッシュして見つける必要がないという点で、ファクトリーの唯一の目的はマイナーな最適化です(とにかく追加はめったにないため、マイナーな最適化です)。 2つの別々の呼び出しで取得および設定します)。
最初にキャッシュを確認し、キャッシュに値が見つからない場合は、何らかのクリティカルセクション(ロック、セマフォなど)を入力し、キャッシュを再確認してください。それでも見つからない場合は、値をフェッチしてください。キャッシュに挿入します。
これにより、バッキングストアが一度だけヒットすることが保証されます。複数の要求が同時にキャッシュミスを受け取った場合でも、最初の要求だけが実際に値をフェッチし、他の要求はセマフォを待って、クリティカルセクションのキャッシュを再チェックするため、早期に戻ります。
擬似コード(非同期で待機できるため、カウント1のSemaphoreSlimを使用):
async Task<TResult> GetAsync(TKey key)
{
// Try to fetch from catch
if (cache.TryGetValue(key, out var result)) return result;
// Get some resource lock here, for example use SemaphoreSlim
// which has async wait function:
await semaphore.WaitAsync();
try
{
// Try to fetch from cache again now that we have entered
// the critical section
if (cache.TryGetValue(key, out result)) return result;
// Fetch data from source (using your HttpClient or whatever),
// update your cache and return.
return cache[key] = await FetchFromSourceAsync(...);
}
finally
{
semaphore.Release();
}
}