.NET实现文件缓存:选用lock加Dictionary还是ConcurrentDictionary?
问题根因
你遇到的重复读取问题是ConcurrentDictionary的GetOrAdd方法的设计导致的:传入的工厂委托并不在字典的内部锁保护范围内。当多个线程同时查询同一个不存在的key时,每个线程都会独立执行工厂方法,直到其中一个线程完成写入后,后续的线程才会直接取到已存在的值,因此会出现多次读文件的情况。
最优解决方案:ConcurrentDictionary + Lazy
这个方案既保留了ConcurrentDictionary细粒度锁的优势,又能保证文件读取逻辑仅执行一次,不会出现大范围阻塞。核心思路是把字典的值包装为Lazy<byte[]>,利用Lazy本身的线程安全特性保证耗时的文件读取仅执行一次:
class Program { private static ConcurrentDictionary<string, Lazy<byte[]>> cache; static void Main(string[] args) { cache = new ConcurrentDictionary<string, Lazy<byte[]>>(); Task.Run(() => { AddToCache("largefile", "largefile.bin"); }); Task.Run(() => { AddToCache("largefile", "largefile.bin"); }); } static byte[] AddToCache(string key, string filename) { var lazyData = cache.GetOrAdd(key, (s) => new Lazy<byte[]>(() => { Console.WriteLine("Reading file into cache"); return File.ReadAllBytes(filename); }, LazyThreadSafetyMode.ExecutionAndPublication) ); return lazyData.Value; } }
方案原理
- 字典的写入操作仅创建轻量的
Lazy<byte[]>实例,就算多个线程同时调用GetOrAdd,创建多个Lazy实例的开销也可以忽略不计。 LazyThreadSafetyMode.ExecutionAndPublication保证Lazy实例的工厂方法(也就是读文件的逻辑)仅会执行一次。- 最终所有线程拿到的都是字典中存储的同一个Lazy实例,读取文件的操作只会触发一次。
方案优势对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 全局lock+Dictionary | 实现简单,保证读文件仅一次 | 锁粒度太大,所有key的读写都争抢同一把锁,高并发下吞吐量低 |
| 原生ConcurrentDictionary | 细粒度锁,吞吐量高 | 同key并发查询时会多次执行耗时的工厂逻辑 |
| ConcurrentDictionary+Lazy | 细粒度锁,吞吐量高,保证读文件仅一次 | 多了一层Lazy包装的极小开销 |
异步场景适配
如果你的文件读取是异步实现,只需要把值改成Lazy<Task<byte[]>>即可:
static async Task<byte[]> AddToCacheAsync(string key, string filename) { var lazyTask = cache.GetOrAdd(key, (s) => new Lazy<Task<byte[]>>(async () => { Console.WriteLine("Reading file into cache"); return await File.ReadAllBytesAsync(filename); }, LazyThreadSafetyMode.ExecutionAndPublication) ); return await lazyTask.Value; }
内容的提问来源于stack exchange,提问作者imekon
相关产品推荐
相关产品推荐

