[C#] 節流策略 - Debounce & Throttle

    最近有一個案子類似搶票功能,雖然公司流量跟台鐵相比真的不足為奇,但因同事們撰寫程式都會發生超賣或太慢的狀況,導致主管總覺得這個很複雜。
當然超賣的根本原因跟節流沒有太大的關係,這部分有機會再紀錄。
主要是設計架構上可以透過節流策略,加強系統的承載能力,所以實作該策略了。


Debounce(去抖動)
● 作用:在事件頻繁觸發時,只在最後一次事件觸發後的一段時間內執行。
● 特點:若在等待時間內再次觸發,計時器重置,直到最後一次觸發後才執行。
● 應用場景:表單輸入自動保存、搜尋框輸入後延遲查詢、網路請求。

Throttle(節流)
● 作用:在固定的時間間隔內限制事件的觸發次數。
● 特點:保證在特定時間段內最多執行一次,即使事件頻繁觸發。
● 應用場景:滾動監聽、按鈕防連點、視窗調整大小事件、網路請求。

Trailing Throttle(結合去抖動和節流)
● 作用:在固定的時間間隔內限制事件的觸發次數,並確保執行最後一次事件
● 特點:保證在特定時間段內最多執行一次,即使事件頻繁觸發,且會執行最後一次事件
● 應用場景:節流情境下,但需要執行最後一次事件


這邊也附上一個js的應用場景(throttle vs debounce),比起rxjs的圖,這應該是最好理解的方式了,大家可以觀察一下兩者的區別。



Debounce(去抖動)

using System;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public static class Debouncer
{
    static ConcurrentDictionary _tokens = new ConcurrentDictionary();

    public static void Debounce(string uniqueKey, Action action, int millisecondsDelay)
    {
		//	如果已經存在 uniqueKey 的 CancellationTokenSource,則取消
        if (_tokens.TryGetValue(uniqueKey, out var existingToken))
        {
            existingToken.Cancel();   // 取消之前的任務
            existingToken.Dispose();  // 釋放資源
        }

		//	創建一個新的 CancellationTokenSource
        var tokenSource = new CancellationTokenSource();
        _tokens[uniqueKey] = tokenSource;

		//	延遲執行
        Task.Delay(millisecondsDelay, tokenSource.Token).ContinueWith(task =>
        {
			//	確保任務未被取消
            if (!task.IsCanceled)
            {
                action(); //  執行傳入的操作
                _tokens.TryRemove(uniqueKey, out _); // 移除完成的任務
            }
        }, TaskScheduler.Default);
    }
}

class Program
{
    static void Main()
    {
		DateTime startDt = DateTime.Now;
		Console.WriteLine($" ********* App Start: {startDt.ToString("HH:mm:ss")} ********* ");

		for (int i = 0; i < 5; i++)
		{
			Console.WriteLine($"Action_{i} trigger");

			// 調用 Debounce,每次調用都會重置計時器
			Debouncer.Debounce("test", () =>
			{
				Console.WriteLine($"Action_{i} executed at: {DateTime.Now.ToString("HH:mm:ss")}");

			}, 1000); // 延遲一秒執行

			Thread.Sleep(300); // 模擬快速連續調用
		}

        Console.ReadLine();
    }
}
  




Throttle(節流)

using System;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public static class Throttler
{
    static ConcurrentDictionary _lastExecutionTimes = new ConcurrentDictionary();

    public static void Throttle(string uniqueKey, Action action, int millisecondsInterval)
    {
		//	獲取當前時間
        var now = DateTime.UtcNow;

        //	計算允許下次執行時間
        if (_lastExecutionTimes.TryGetValue(uniqueKey, out var lastExecutionTime))
        {
            //	檢查是否已到下次可執行時間
            if ((now - lastExecutionTime).TotalMilliseconds < millisecondsInterval)
            {
                return; //	時間未到,忽略當前調用
            }
        }

        //	紀錄新的執行時間
        _lastExecutionTimes[uniqueKey] = now;

        //	執行操作
        action();
    }
}

class Program
{
    static void Main()
    {
        DateTime startDt = DateTime.Now;
		Console.WriteLine($" ********* App Start: {startDt.ToString("HH:mm:ss")} ********* ");

		for (int i = 0; i < 15; i++)
		{
			Console.WriteLine($"Action_{i} trigger");

			// 調用 Debounce,每次調用都會重置計時器
			Throttler.Throttle("test", () =>
			{
				Console.WriteLine($"Action_{i} executed at: {DateTime.Now.ToString("HH:mm:ss")}");

			}, 1000); // 延遲一秒執行

			Thread.Sleep(300); // 模擬快速連續調用
		}

        Console.ReadLine();
    }
}
  




Trailing Throttle(結合去抖動和節流,執行最後一次)

using System;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public static class TrailingThrottler
{
    private static ConcurrentDictionary _lastExecutionTimes = new ConcurrentDictionary();
    private static ConcurrentDictionary _trailingCalls = new ConcurrentDictionary();

    public static void Throttle(string uniqueKey, Action action, int millisecondsInterval)
    {
        var now = DateTime.UtcNow;

        // 檢查上次執行時間
        if (_lastExecutionTimes.TryGetValue(uniqueKey, out var lastExecutionTime))
        {
            // 如果未達到時間間隔,延遲執行最後一次
            if ((now - lastExecutionTime).TotalMilliseconds < millisecondsInterval)
            {
				#region 最後一次調用
                //	取消之前的延遲操作,並重新安排後一次調用
                if (_trailingCalls.TryGetValue(uniqueKey, out var cts))
                {
                    cts.Cancel();
                }

                // 創建新的延遲操作
                var newCts = new CancellationTokenSource();
                _trailingCalls[uniqueKey] = newCts;

                // 延遲執行最後一次
                Task.Delay(millisecondsInterval, newCts.Token).ContinueWith(task =>
                {
					//	若沒有被取消(備註: 上面的cts.Cancel()),就會執行
                    if (!task.IsCanceled)
                    {
                        action(); //	設定執行最後一次操作
                        _lastExecutionTimes[uniqueKey] = DateTime.UtcNow;
                        _trailingCalls.TryRemove(uniqueKey, out _); // 移除追蹤
                    }
                }, TaskScheduler.Default);
				#endregion

                return;	//	一般的調用,就忽略跳過
            }
        }

        //	更新上次執行時間,並立即執行操作
        _lastExecutionTimes[uniqueKey] = now;
        action();
    }
}

class Program
{
    static void Main()
    {
        DateTime startDt = DateTime.Now;
		Console.WriteLine($" ********* App Start: {startDt.ToString("HH:mm:ss")} ********* ");

		for (int i = 0; i < 15; i++)
		{
			Console.WriteLine($"Action_{i} trigger");

			// 調用 Debounce,每次調用都會重置計時器
			TrailingThrottler.Throttle("test", () =>
			{
				Console.WriteLine($"Action_{i} executed at: {DateTime.Now.ToString("HH:mm:ss")}");

			}, 1000); // 延遲一秒執行

			Thread.Sleep(300); // 模擬快速連續調用
		}

        // 保持控制台窗口打开
        Console.ReadLine();
    }
}








留言

這個網誌中的熱門文章

[Note] 公司常見的書信結尾

[C#] 無法載入檔案或組件 或其相依性的其中之一。 找到的組件資訊清單定義與組件參考不符。 (發生例外狀況於 HRESULT: 0x80131040)

[IIS] 拒絕存取路徑 & 開啟資料夾權限