.NET 固定時間視窗演算法實現(無鎖執行緒安全)

曉晨Master發表於2022-02-17

一.前言

最近有一個生成 APM TraceId 的需求,公司的APM系統的 TraceId 的格式為:APM AgentId+毫秒級時間戳+自增數字,根據此規則生成的 Id 可以保證全域性唯一(有 NTP 時間同步),前兩個欄位好說,最後一個欄位也不復雜,我的想法是按秒來進行自增。比如說1秒的時候,自增計數為100,在2秒的時候會重置為0,然後進行自增。其實這個思想就是固定時間視窗演算法,這個演算法一般常用在限流、Id生成器等場景。

二.程式碼實現

long _currentTime;
long _current;
public long FixedWindow()
{
    var now = DateTimeOffset.Now.ToUnixTimeSeconds();
    var ct = Interlocked.Read(ref _currentTime);
    if (now > ct)
    {
        if (Interlocked.CompareExchange(ref _currentTime, now, ct)==ct)
        {
            return Interlocked.Exchange(ref _current, 0);
        }
    }
    
    return Interlocked.Increment(ref _current);
}

程式碼沒多少,每呼叫一次就返回計數,採用的 C# CAS API Interlocked ,保證每個計數操作都是原子操作,從而達到無鎖。

測試程式碼,使用10個執行緒併發呼叫,每個執行緒呼叫 1w次,最終期望計數應該是10w。

private static long _currentTime;
private static long _current;
private static Semaphore _semaphore = new Semaphore(0, 10);
static void Main(string[] args)
{
    _currentTime = DateTimeOffset.Now.ToUnixTimeSeconds();
    _current = 0;
    for (int i = 0; i < 10; i++)
    {
        Task.Factory.StartNew(() =>
        {
            for (int j = 0; j < 10000; j++)
            {
                FixedWindow();
            }

            _semaphore.Release(1);
        });
    }

    for (int i = 0; i < 10; i++)
    {
        _semaphore.WaitOne();
    }
    
    Console.WriteLine(_current);
    Console.WriteLine("sleep 2s");
    Thread.Sleep(2000);
    Console.WriteLine(FixedWindow());
}

執行結果:

image-20220217141347106

符合預期,10執行緒的計數在 1s 內能執行完畢,所以最終計數是10w,然後sleep 2s,重置計數,再次呼叫就返回了 1

三.資料

本文 Demo 程式碼:github

相關文章