عبارات باقاعده و نیاز به Timeout
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه


یکبار سعی کنید مثال ساده زیر را اجرا کنید:

using System;
using System.Text.RegularExpressions;

namespace RegexLoop
{
class Program
{
static void Main(string[] args)
{
var emailAddressRegex = new Regex(@"^[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\@[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\.[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*$|^$");
if (emailAddressRegex.IsMatch("an.infinite.loop.sample.just_for.test"))
{
Console.WriteLine("Matched!");
}

var input = "The quick brown fox jumps";
var pattern = @"([a-z ]+)*!";
if (Regex.IsMatch(input, pattern))
{
Console.WriteLine("Matched!");
}
}
}
}


پس از اجرا، برنامه هنگ خواهد کرد یا به عبارتی برنامه در یک حلقه بی‌نهایت قرار می‌گیرد (در هر دو مثال؛ اطلاعات بیشتر و آنالیز کامل در اینجا). بنابراین نیاز به مکانیزمی امنیتی جهت محافظت در برابر این نوع ورودی‌ها وجود خواهد داشت؛ مثلا یک Timeout . اگر تا 2 ثانیه به جواب نرسیدیم، اجرای Regex متوقف شود. تا دات نت 4، چنین timeout ایی پیش بینی نشده؛ اما در دات نت 4 و نیم آرگومانی جهت تعریف حداکثر مدت زمان قابل قبول اجرای یک عبارت باقاعده در نظر گرفته شده است (^) و اگر در طی مدت زمان مشخص شده، کار انجام محاسبات به پایان نرسد، استثنای RegexMatchTimeoutException صادر خواهد شد.
خیلی هم خوب. به این ترتیب کسی نمی‌تونه با یک ورودی ویژه، CPU Usage سیستم رو تا مدت زمان نامحدودی به 100 درصد برساند و عملا استفاده از سیستم رو غیرممکن کنه.
اما تا قبل از دات نت 4 و نیم چکار باید کرد؟ روش کلی حل این مساله به این ترتیب است که باید اجرای Regex را به یک ترد دیگر منتقل کرد؛ اگر مدت اجرای عملیات، از زمان تعیین شده بیشتر گردید، آنگاه می‌شود ترد را Abort کرد و به عملیات خاتمه داد. روش پیاده سازی و نحوه استفاده از آن‌را در ادامه ملاحظه خواهید نمود:

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

namespace RegexLoop
{
public static class TimedRunner
{
public static R RunWithTimeout<R>(Func<R> proc, TimeSpan duration)
{
using (var waitHandle = new AutoResetEvent(false))
{
var ret = default(R);
var thread = new Thread(() =>
{
ret = proc();
waitHandle.Set();
}) { IsBackground = true };
thread.Start();

bool timedOut = !waitHandle.WaitOne(duration, false);
waitHandle.Close();

if (timedOut)
{
try
{
thread.Abort();
}
catch { }
return default(R);
}
return ret;
}
}
}

class Program
{
static void Main(string[] args)
{
var emailAddressRegex = new Regex(@"^[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\@[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*\.[A-Za-z0-9]([_\.\-]?[A-Za-z0-9]+)*$|^$");
if (TimedRunner.RunWithTimeout(
() => emailAddressRegex.IsMatch("an.infinite.loop.sample.just_for.test"),
TimeSpan.FromSeconds(2)))
{
Console.WriteLine("Matched!");
}

var input = "The quick brown fox jumps";
var pattern = @"([a-z ]+)*!";
if (TimedRunner.RunWithTimeout(() => Regex.IsMatch(input, pattern), TimeSpan.FromSeconds(2)))
{
Console.WriteLine("Matched!");
}
}
}
}

اینبار به هر کدام از عبارات باقاعده 2 ثانیه زمان برای اتمام کار داده شده است. در غیراینصورت مقدار پیش فرض خروجی متد فراخوانی شده، بازگشت داده می‌شود که در اینجا false است.

  • #
    ‫۱۱ ماه قبل، جمعه ۲۸ مهر ۱۴۰۲، ساعت ۰۴:۰۴
    نکته تکمیلی:

    از NET 6. به بعد نیز میتوانیم مقدار Timeout را به صورت سراسری تنظیم کنیم:
    AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMicroseconds(50));
    مقدار پیش‌فرض آن  Regex.InfiniteMatchTimeout میباشد یعنی تا زمان اتمام تطبیق ادامه خواهد داشت و اگر در حال نگارش کتابخانه‌ای هستید، بهتر است از روش زیر استفاده کنید:
    Regex.IsMatch("abc", "a", RegexOptions.None, TimeSpan.FromSeconds(5));
    این قابلیت از زمان دات‌نت 4.5 اضافه شده‌است.
  • #
    ‫۱۰ ماه قبل، چهارشنبه ۳ آبان ۱۴۰۲، ساعت ۱۰:۴۷
    روش پیاده سازی Timeout در نگارش‌های جدیدتر #C
    public static class TimeoutExtensions
    {
         private static void ExecuteTimeoutCommon(Task actionTask, TimeSpan maxDelay)
         {
             var delayTask = Task.Delay(maxDelay);
             var finishedTaskIndex = Task.WaitAny(actionTask, delayTask);
             if (finishedTaskIndex != 0)
             {
                 throw new TimeoutException("Action did not finish in the desired time slot.");
             }
         }
    
         public static void ExecuteTimeout<T>(Func<T> func, TimeSpan maxDelay)
         {
             var executionTask = Task.Run(() =>
             {
                 func();
             });
             ExecuteTimeoutCommon(executionTask, maxDelay);
         }
    
         public static void ExecuteTimeout(Action action, TimeSpan maxDelay)
         {
             var executionTask = Task.Run(() =>
             {
                 action();
             });
             ExecuteTimeoutCommon(executionTask, maxDelay);
         }
    }
    با این مثال:
    TimeoutExtensions.ExecuteTimeout(Console.ReadLine, TimeSpan.FromSeconds(3));