A cache retains previously obtained values so later requests can avoid repeating an expensive operation. A typical application cache stores HTTP results in a Dictionary keyed by the request, while CPU cache memory stores recently accessed data closer to processor cores. section 5.3 section 7.2
Protect a cache according to the work it performs:
- Lock only the dictionary access before and after an await; holding a lock across await is invalid because continuation code may run on another thread and can cause deadlocks. section 5.3
- Expect concurrent misses when the lock is released during the HTTP call: multiple callers can fetch the same value, and later completions overwrite earlier ones. section 5.3
- Use synchronization rather than homemade flags for shared state; compiler reordering, stale CPU cache values, and per-core caches can make unsynchronized observations incorrect. section 7.2
- Treat volatile cautiously: it does not guarantee access to the latest value and may introduce additional problems. section 7.2
Listing 5.10 Releasing the lock while awaiting
private Dictionary<string,string> _cache = new();
private object _cacheLock = new();
public async Task<string> GetResult(string query)
{
lock(_cacheLock) #1
{
if(_cache.TryGetValue(query, out var cacheResult)
return cacheResult;
}
var http = new HttpClient();
var result = await http.GetStringAsync( #2
"https://example.com?"+query); #2
lock(_cacheLock) #3
{
_cache[query] = result;
}
return result;
}
Shows a C# cache implementation that checks the dictionary under a lock, performs the asynchronous HTTP request outside it, and locks again when storing the result. section 5.3
Concurrent asynchronous lookup
Two callers request the same missing query. The first caller observes a miss and releases the lock before awaiting the HTTP request. The second caller also observes a miss and starts another request. When the calls finish, each updates the cache; the later update replaces the earlier value. section 5.3





































