[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();
}
}



留言
張貼留言