某些正則表達式對特定輸入會觸發**災難性回溯**,導致 CPU 100%、執行緒卡死、無異常拋出、應用程式凍結。典型特徵:
.NET Framework / .NET Core **預設無內建逾時機制**(.NET 5+ `RegexOptions.NonBacktracking` 可緩解但非萬靈丹),需自行實作「執行緒級超時中止」。
核心思想:**將 Regex 匹配放入獨立執行緒,主執行緒輪詢等待,超時則 `Thread.Abort()` 強制中止。**
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 引入 **非回溯引擎**(基於有限自動機),**從根源消除災難性回溯**,執行時間與輸入長度呈**線性關係**。
// 僅需加上選項,無需額外執行緒/超時邏輯
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 或需完整語法時的備選 |
正則表達式威力強大,卻隱藏「災難性回溯」陷阱。**`AsynchronousRegex` 以執行緒隔離 + 超時中止提供通用防禦**,但 `.NET 5+ NonBacktracking` 引擎才是**根治之道**。根據專案框架版本、正則複雜度、相容性需求,選擇最適合的策略,讓 Regex 永遠是得力助手,而非效能殺手。