首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 正則表達式災難性回溯防禦:非同步超時中止方案 AsynchronousRegex

     編輯:關於C語言

問題:當 Regex 變成「CPU 吃到飽」

某些正則表達式對特定輸入會觸發**災難性回溯**,導致 CPU 100%、執行緒卡死、無異常拋出、應用程式凍結。典型特徵:

  • 巢狀量詞:`(a+)+`、`(a*)*`
  • 重疊選項:`(a|a?)+`、`(.*a)+`
  • 輸入長且不匹配(如長字串無預期模式)

.NET Framework / .NET Core **預設無內建逾時機制**(.NET 5+ `RegexOptions.NonBacktracking` 可緩解但非萬靈丹),需自行實作「執行緒級超時中止」。

方案架構:AsynchronousRegex

核心思想:**將 Regex 匹配放入獨立執行緒,主執行緒輪詢等待,超時則 `Thread.Abort()` 強制中止。**

  • ✅ 可設定任意超時毫秒數
  • ✅ 不阻塞呼叫執行緒(UI/請求執行緒)
  • ✅ 超時後回傳 `null` / 空集合,並暴露 `IsTimeout` 旗標
  • ⚠️ `Thread.Abort()` 為**強制終止**,可能留下不一致狀態(鎖未釋放、非託管資源洩漏),**僅建議用於純運算型 Regex 匹配**,不可用於資料庫/IO/關鍵區段。

完整實作程式碼(現代化重構)

using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

public class AsynchronousRegex
{
    private readonly int _timeoutMs;
    private readonly int _pollIntervalMs = 50; // 較原版 100ms 更精準
    private MatchCollection _matches;
    private bool _isTimeout;

    public bool IsTimeout => _isTimeout;
    public MatchCollection Matches => _matches;

    public AsynchronousRegex(int timeoutMs)
    {
        if (timeoutMs <= 0) throw new ArgumentOutOfRangeException(nameof(timeoutMs));
        _timeoutMs = timeoutMs;
    }

    /// 
    /// 同步阻塞呼叫(內部仍用執行緒隔離)
    /// 
    public MatchCollection Match(Regex regex, string input)
    {
        if (regex == null) throw new ArgumentNullException(nameof(regex));
        if (input == null) throw new ArgumentNullException(nameof(input));

        _matches = null;
        _isTimeout = false;

        var worker = new RegexWorker(regex, input);
        worker.MatchCompleted += OnMatchCompleted;

        Thread thread = new Thread(worker.Execute) { IsBackground = true };
        thread.Start();

        // 主執行緒輪詢等待
        int elapsed = 0;
        while (thread.IsAlive)
        {
            if (elapsed >= _timeoutMs)
            {
                try { thread.Abort(); } catch { /* 忽略 Abort 失敗 */ }
                _isTimeout = true;
                break;
            }
            Thread.Sleep(_pollIntervalMs);
            elapsed += _pollIntervalMs;
        }

        // 確保執行緒結束
        thread.Join(TimeSpan.FromMilliseconds(100));
        return _matches;
    }

    /// 
    /// 非同步版本(推薦用於 UI / ASP.NET Core)
    /// 
    public async Task MatchAsync(Regex regex, string input, CancellationToken cancellationToken = default)
    {
        if (regex == null) throw new ArgumentNullException(nameof(regex));
        if (input == null) throw new ArgumentNullException(nameof(input));

        _matches = null;
        _isTimeout = false;

        var worker = new RegexWorker(regex, input);
        worker.MatchCompleted += OnMatchCompleted;

        Thread thread = new Thread(worker.Execute) { IsBackground = true };
        thread.Start();

        // 使用 TaskCompletionSource 橋接執行緒與 async/await
        var tcs = new TaskCompletionSource();
        worker.MatchCompleted += (mc) => tcs.TrySetResult(mc);

        // 超時取消
        var timeoutCts = new CancellationTokenSource(_timeoutMs);
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
        linkedCts.Token.Register(() => 
        {
            if (thread.IsAlive)
            {
                try { thread.Abort(); } catch { }
                _isTimeout = true;
                tcs.TrySetCanceled();
            }
        });

        try
        {
            _matches = await tcs.Task;
            return _matches;
        }
        catch (OperationCanceledException) when (_isTimeout)
        {
            return null; // 或拋出自訂 TimeoutException
        }
        finally
        {
            timeoutCts.Dispose();
            thread.Join(TimeSpan.FromMilliseconds(100));
        }
    }

    private void OnMatchCompleted(MatchCollection mc)
    {
        _matches = mc;
    }

    // 內部執行緒工作類別
    private class RegexWorker
    {
        private readonly Regex _regex;
        private readonly string _input;
        public event Action MatchCompleted;

        public RegexWorker(Regex regex, string input)
        {
            _regex = regex;
            _input = input;
        }

        public void Execute()
        {
            try
            {
                var mc = _regex.Matches(_input);
                MatchCompleted?.Invoke(mc);
            }
            catch (ThreadAbortException)
            {
                Thread.ResetAbort(); // 吞掉 Abort,讓執行緒安靜結束
                MatchCompleted?.Invoke(null);
            }
            catch (Exception)
            {
                MatchCompleted?.Invoke(null);
            }
        }
    }
}

使用範例

// 同步呼叫(主控台/背景服務)
var asyncRegex = new AsynchronousRegex(2000); // 2 秒超時
var regex = new Regex(@"(a+)+b"); // 經典災難性回溯模式
string evilInput = new string('a', 10000) + "c"; // 不匹配,會回溯爆炸

MatchCollection matches = asyncRegex.Match(regex, evilInput);
if (asyncRegex.IsTimeout)
{
    Console.WriteLine("Regex 匹配超時,已中止");
}
else if (matches != null)
{
    Console.WriteLine($"匹配 {matches.Count} 個結果");
}

// 非同步呼叫(WinForms/WPF/ASP.NET Core)
public async Task ProcessAsync(string input)
{
    var asyncRegex = new AsynchronousRegex(1000);
    var matches = await asyncRegex.MatchAsync(new Regex(@"(\w+\s?)+"), input);
    if (asyncRegex.IsTimeout)
    {
        _logger.LogWarning("Regex 超時:{Input}", input.Substring(0, 50));
        return;
    }
    // 處理 matches...
}

.NET 5+ 現代替代方案:`RegexOptions.NonBacktracking`

.NET 5 引入 **非回溯引擎**(基於有限自動機),**從根源消除災難性回溯**,執行時間與輸入長度呈**線性關係**。

// 僅需加上選項,無需額外執行緒/超時邏輯
var regex = new Regex(@"(a+)+b", RegexOptions.NonBacktracking | RegexOptions.Compiled);
MatchCollection matches = regex.Matches(evilInput); // 瞬間完成,不卡死
特性`NonBacktracking``AsynchronousRegex` (Thread.Abort)
相容性.NET 5+.NET Framework 2.0+ / .NET Core / .NET 5+
原理編譯為 DFA,無回溯執行緒隔離 + 強制中止
支援語法子集(不支援回溯引用 `\1`、條件、平衡群組等)完整 Regex 語法
效能線性、穩定、無超時風險有輪詢/Abort 開銷,極端情況仍可能殘留狀態
建議**優先選擇**(語法相容時)舊版 .NET 或需完整語法時的備選

最佳實踐總結

  1. **靜態分析**:Code Review 階段檢視正則是否含巢狀量詞、重疊選項。
  2. **單元測試**:對「長字串不匹配」案例測試效能,設定基準門檻。
  3. **.NET 5+ 專案**:優先使用 `RegexOptions.NonBacktracking` + `RegexOptions.Compiled`。
  4. **舊版專案 / 複雜語法**:引入 `AsynchronousRegex` 非同步超時中止,並**嚴格限制用於純 CPU 運算匹配**。
  5. **監控告警**:生產環境記錄 Regex 執行耗時、超時次數,異常飆升即告警。

結語

正則表達式威力強大,卻隱藏「災難性回溯」陷阱。**`AsynchronousRegex` 以執行緒隔離 + 超時中止提供通用防禦**,但 `.NET 5+ NonBacktracking` 引擎才是**根治之道**。根據專案框架版本、正則複雜度、相容性需求,選擇最適合的策略,讓 Regex 永遠是得力助手,而非效能殺手。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved