雙重檢查鎖定模式 (Double-Checked Locking Pattern)

雙重檢查鎖定 (Double-Checked Locking Pattern) 是另外一個常用的設計模式,用來減少並發系統中競爭和同步的開銷。雙重檢查鎖定模式首先驗證鎖定條件(第一次檢查),只有通過鎖定條件驗證才真正的進行加鎖邏輯並再次驗證條件(第二次檢查)。常用來避免快取在同一時間被重複建立。

定義

我們先來看看來自 Wiki 的定義

雙重檢查鎖定模式(也被稱為”雙重檢查加鎖優化”,”鎖暗示”(Lock hint)[1]) 是一種軟體設計模式用來減少並發系統中競爭和同步的開銷。雙重檢查鎖定模式首先驗證鎖定條件(第一次檢查),只有通過鎖定條件驗證才真正的進行加鎖邏輯並再次驗證條件(第二次檢查)。 該模式在某些語言在某些硬體平台的實現可能是不安全的。有的時候,這一模式被看做是反模式。 它通常用於減少加鎖開銷,尤其是為多執行緒環境中的單例模式實現「惰性初始化」。惰性初始化的意思是直到第一次訪問時才初始化它的值。

C# 實際範例

在一般的情境下,我最常使用在快取建立的情境,在快取要建立前,使用 Double-Checked Locking 防止快取的重複建立。

使用快取範例

先來說明 ASP.NET Core 下如何使用快取,範例是使用 WebApi 專案範本。

Startup 註冊 MemoryCache

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register MemoryCache
        services.AddMemoryCache();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvcWithDefaultRoute();
    }
}

再來 Contorller 以下設定

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;

namespace DoubleCheckedLocking.WebApplication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private IMemoryCache _cache;

        public ValuesController(IMemoryCache memoryCache)
        {
            this._cache = memoryCache;
        }

            // 取得快取值
            var cacheEntry = this._cache.Get<DateTime>("key");

            // 快取檢查
            if (this._cache.Get<DateTime>("key") == null)
            {
                // 無快取, 所以重新取值
                cacheEntry = DateTime.Now;

                // 設定快取過期時間
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                    .SetAbsoluteExpiration(TimeSpan.FromSeconds(3));

                // 加入快取
                _cache.Set("key", cacheEntry, cacheEntryOptions);
            }

            return cacheEntry.ToString();
        }
    }
}

會遇到的問題

當大量連線請求執行這段程式碼的時候,是有可能在極短暫時間下,快取不存在的鎖定判斷會被通過多次請求,而導致快取被重複建立,而當快取建立的成本很高的時候(比如說連接資料庫),更可能造成後端的負載壓力,在筆者工作環境中,是個同時上線人數達 2000 人的大系統是,屢屢遇到快取鎖定失效導致資料庫負擔過重的案例,這時候 Double-Checked Locking 便派上用場,讓我們將快取的程式碼繼續導入 Double-Checked Locking。

    // GET api/values
    [HttpGet]
    public ActionResult<string> Get()
    {
        // 取得快取值
        var cacheEntry = this._cache.Get<DateTime>("key");

        // 第一次檢查
        if (cacheEntry == null)
        {
            // 鎖定
            lock (padlock)
            {
                // 第二次檢查
                if (this._cache.Get<DateTime>("key") == null)
                {
                    // 無快取, 所以重新取值
                    cacheEntry = DateTime.Now;

                    // 設定快取過期時間
                    var cacheEntryOptions = new MemoryCacheEntryOptions()
                        .SetAbsoluteExpiration(TimeSpan.FromSeconds(3));

                    // 加入快取
                    _cache.Set("key", cacheEntry, cacheEntryOptions);
                }
            }
        }

        return cacheEntry.ToString();
    }
}

這裡要注意的是,第二次的檢查務必重新取得快取來判斷,若繼續沿用第一次檢查的變數,將會導致無效檢查。

結語

Double-Checked Locking 在快取情境下使用非常適合,另外一個適用情境是單例模式 (Singleton Pattern),之後講到 Singleton 再說明囉。

程式碼範例

Last updated