先前筆者發表《無縫快取讀取:雙儲存策略》,採用兩個儲存位址輪替以達無鎖讀取。經網友 Cat Chen 指點,發現雙指標切換其實屬過度設計 —— 單一儲存位址配合寫入時的執行緒同步,即可達成同樣的無縫讀取效果。本文將展示簡化後的架構與程式碼。
拋棄雙緩衝區指標,改用單一儲存槽加上狀態旗標:
namespace CHCache
{
/// 快取介質
public class Medium
{
/// 儲存區:存放實際快取實體
public object Store { get; set; }
/// 是否正在更新中
public bool IsUpdating { get; set; }
}
}
Store 保存快取資料本體;IsUpdating 標記該槽位是否正在進行寫入操作,讀取端據此判斷是否命中有效資料。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace CHCache
{
public class DictionaryCache : IEnumerable
{
public DictionaryCache()
{
Store = new Dictionary<string, Medium>();
}
/// 新增/更新快取項目
/// 快取鍵值
/// 資料載入委派 (Lambda 表達式)
public void Add(string key, Func<object> func)
{
if (Store.ContainsKey(key))
{
var elem = Store[key];
if (elem.IsUpdating) return; // 正在更新,略過本次請求
// 啟動背景執行緒重新載入
var th = new ThreadHelper(elem, func);
var td = new Thread(th.Doit);
td.Start();
}
else
{
// 首次寫入:主執行緒同步執行,確保立即可用
Console.WriteLine("Begin first write");
Store.Add(key, new Medium { Store = func() });
Console.WriteLine("End first write");
}
}
/// 讀取快取資料
public object this[string key]
{
get
{
if (!Store.ContainsKey(key)) return null;
return Store[key].Store; // 直接回傳,無鎖讀取
}
}
public Dictionary<string, Medium> Store { get; set; }
public IEnumerator GetEnumerator()
{
return ((IEnumerable)Store).GetEnumerator();
}
}
}
func() 取得資料。IsUpdating == false 時,啟動新執行緒執行 ThreadHelper.Doit,不阻塞呼叫端。Store 參考,完全無鎖,極適合高併發讀取場景。using System;
namespace CHCache
{
public class ThreadHelper
{
public Func<object> Fun { get; set; }
public Medium Medium { get; set; }
public ThreadHelper(Medium m, Func<object> fun)
{
Medium = m;
Fun = fun;
}
public void Doit()
{
Medium.IsUpdating = true;
Console.WriteLine("Begin write.");
var ret = Fun.Invoke(); // 執行使用者提供的資料載入邏輯
Medium.Store = ret; // 原子性替換參考
Console.WriteLine("End write.");
Medium.IsUpdating = false;
}
}
}
Doit 方法流程清晰:設定旗標 → 執行載入 → 替換儲存體參考 → 清除旗標。參考替換在 .NET 中屬原子操作,讀取端要么看到舊資料,要么看到新資料,絕不會讀到半殘損狀態。
這是一個典型的「由簡入繁、由繁返簡」過程。初期受雙緩衝區思想影響,刻意引入指標切換機制;經同好提點後才發現,單一儲存槽配合 IsUpdating 旗標即能滿足「寫入不阻塞讀取、讀取不阻塞寫入」的核心需求。實務開發中,這類過度工程現象屢見不鮮,保持對本質問題的敏銳度至關重要。
ConcurrentDictionary)完整原始碼可參考筆者部落格附件下載。這個精簡版本不僅程式碼行數減少 40%,更消除了雙指標切換帶來的潛在競態風險,是實務專案中極佳的輕量級快取基石。