مدیریت منابع با Task.Dispose در برنامهنویسی ناهمزمان #C
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۰۳ ۰۹:۲۹
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Task در #C ابزاری بنیادین برای پیادهسازی عملیات ناهمزمان فراهم میکند. توسعهدهندگان اغلب با الگوهای async/await و عملیاتهای پایه Task آشنا هستند، اما متد Dispose در کلاس Task معمولاً مبهم باقی میماند. این مقاله به بررسی عمیق Task.Dispose، زمان و چرایی نیاز به آن، و بهترین شیوههای مرتبط میپردازد تا به توسعهدهندگان داتنت در مدیریت کارآمدتر منابع کمک کند.Task.Disposeو موارد استفادهTask واسط IDisposable را پیادهسازی میکند، به این معنی که دارای یک متد Dispose است. با این حال، برخلاف بسیاری از اشیاء Disposable دیگر، به ندرت نیاز است که متد Dispose را مستقیماً روی یک Task فراخوانی کنید. دلیل این امر در نحوه مدیریت Task توسط زمان اجرای داتنت (DotNet Runtime) نهفته است.public class Task : IAsyncResult, IDisposable
{
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}Dispose در کلاس Task از الگوی معمول IDisposable پیروی میکند. اما پرسش اصلی این است که چه زمانی باید یک Task را Dispose کنیم؟Dispose کردن یک Task، جلوگیری از پرتاب استثناهای مشاهده نشده در نخ نهاییکننده (Finalizer Thread) است. هنگامی که یک Task با یک استثنا به پایان میرسد و این استثنا هرگز توسط کدی که Task را اجرا کرده، مشاهده (Observe) یا مدیریت نمیشود، زمان اجرای داتنت ممکن است در نهایت این استثنا را دوباره در نخ نهاییکننده پرتاب کند. این رفتار میتواند منجر به خرابی برنامه در برخی سناریوها شود. با Dispose کردن Task، میتوان از این اتفاق جلوگیری کرد. به عنوان مثال:public async Task ProcessDataAsync()
{
var task = Task.Run(() =>
{
throw new Exception("This exception might be unobserved!");
});
// اگر Task را await نکنیم یا آن را مدیریت نکنیم، باید آن را Dispose کنیم
task.Dispose(); // از پرتاب استثنای مشاهده نشده جلوگیری میکند
}task نه await شود و نه نتیجه آن به نحوی دسترسی پیدا کند (که منجر به مشاهده استثنا میشود)، Dispose کردن آن میتواند از بروز استثنای مشاهده نشده جلوگیری کند.Dispose میتواند مفید باشد. این معمولاً زمانی اتفاق میافتد که یک کلاس دیگر، یک Task را برای انجام یک فرآیند پسزمینه نگهداری میکند و آن کلاس خودش IDisposable را پیادهسازی میکند تا منابع را آزاد کند.public class DataProcessor : IDisposable
{
private Task _processingTask;
public void StartProcessing()
{
_processingTask = Task.Run(() =>
{
// پردازش طولانی مدت
while (true)
{
// پردازش داده
}
});
}
public void Dispose()
{
_processingTask?.Dispose();
}
}DataProcessor یک Task به نام _processingTask را نگه میدارد. وقتی یک نمونه از DataProcessorDispose میشود، متد Dispose آن، متد Dispose مربوط به _processingTask را فراخوانی میکند تا اطمینان حاصل شود که هرگونه منبع مرتبط با آن Task، به درستی آزاد میشود.Task.Dispose وجود دارد که میتواند منجر به استفاده نادرست از آن شود.Disposeکردن هرTaskدارم"Dispose کردن Task ها نیست. زمان اجرای داتنت، پاکسازی Task را به صورت خودکار و کارآمد در موارد زیر انجام میدهد:Task: هنگامی که Task بدون استثنا به پایان میرسد.Task مورد await قرار میگیرد: با استفاده از کلمه کلیدی await، استثناهای Task در همان کانتکس (Context) کد مدیریت میشوند.Task مورد دسترسی قرار میگیرد: دسترسی به ویژگی Result یک Task یا فراخوانی Wait()، استثناهای موجود را پرتاب میکند و Task را مشاهده میکند.Dispose نیست:// نیازی به Dispose نیست - Task به درستی مدیریت میشود
public async Task<string> GetDataAsync()
{
var task = Task.Run(() => "Data");
return await task;
}
// نیازی به Dispose نیست - نتیجه مورد دسترسی قرار میگیرد
public string GetData()
{
var task = Task.Run(() => "Data");
return task.Result;
}Task به نحوی مدیریت میشود که زمان اجرای داتنت میتواند آن را به درستی پاکسازی کند.Disposeکردن یکTask آن را لغو میکند"Dispose کردن یک Task آن را لغو نمیکند. اگر نیاز به لغو یک Task دارید، باید از CancellationToken (توکن لغو) استفاده کنید. این ابزار استاندارد در داتنت برای هماهنگسازی لغو عملیات ناهمزمان است.public async Task ProcessWithCancellationAsync(CancellationToken cancellationToken)
{
var task = Task.Run(() =>
{
// به صورت دورهای توکن لغو را بررسی کنید
while (!cancellationToken.IsCancellationRequested)
{
// پردازش داده
}
}, cancellationToken);
await task;
}CancellationToken به Task امکان میدهد تا وضعیت لغو را بررسی کرده و عملیات خود را به طور منظم متوقف کند، در حالی که Dispose صرفاً به آزاد کردن منابع اختصاص دارد.Task.Dispose کمتر مورد استفاده قرار میگیرد، درک بهترین شیوهها برای مدیریت Task و منابع مربوط به آن ضروری است.usingباTaskDispose کردن یک Task دارید، بهترین روش استفاده از دستور using است. این الگو تضمین میکند که متد Dispose حتی در صورت بروز استثنا، فراخوانی میشود.public async Task ProcessWithDisposalAsync()
{
using (var task = Task.Run(() =>
{
// برخی پردازشها
}))
{
// با Task کار کنید
await task;
} // Task به صورت خودکار Dispose میشود
}Task پس از خروج از بلوک using، به درستی Dispose میشود.Dispose برای جلوگیری از استثناهای مشاهده نشده، بهتر است یک مکانیسم سراسری برای مدیریت آنها داشته باشید. TaskScheduler.UnobservedTaskException یک رویداد سراسری است که به شما امکان میدهد استثناهای Task را که مشاهده نشدهاند، در یک مکان مرکزی ثبت و مدیریت کنید.TaskScheduler.UnobservedTaskException += (sender, args) =>
{
// استثنا را ثبت کنید
Console.WriteLine($"Unobserved exception: {args.Exception}");
// به عنوان مدیریت شده علامتگذاری کنید
args.SetObserved();
};args.SetObserved() به زمان اجرا اعلام میکند که استثنا مدیریت شده است و از پرتاب مجدد آن جلوگیری میکند.TaskCompletionSourceTask و وضعیت آن، TaskCompletionSource ابزار قدرتمندی است. این کلاس به شما امکان میدهد یک Task ایجاد کنید و آن را به صورت دستی تکمیل، خطا یا لغو کنید. در سناریوهایی که نیاز به مدیریت پیچیدهتر Task دارید، ممکن است نیاز به Dispose کردن Task مرتبط با TaskCompletionSource باشد.public class CustomTaskManager : IDisposable
{
private readonly TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();
public Task<bool> Operation => _tcs.Task;
public void CompleteOperation(bool result)
{
_tcs.TrySetResult(result);
}
public void Dispose()
{
_tcs.TrySetCanceled();
_tcs.Task.Dispose();
}
}CustomTaskManager از TaskCompletionSource برای مدیریت یک عملیات ناهمزمان استفاده میکند. متد Dispose آن اطمینان میدهد که Task مرتبط لغو شده و Dispose میشود.Task.Dispose در یک سناریوی واقعی، یک کلاس BackgroundProcessor را در نظر بگیرید که یک عملیات طولانی مدت را در پسزمینه اجرا میکند.public class BackgroundProcessor : IDisposable
{
private Task _processingTask;
private CancellationTokenSource _cts;
public void StartProcessing()
{
_cts = new CancellationTokenSource();
_processingTask = Task.Run(async () =>
{
while (!_cts.Token.IsCancellationRequested)
{
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
// استثنا را ثبت کنید اما به پردازش ادامه دهید
Console.WriteLine($"Error: {ex.Message}");
}
}
}, _cts.Token);
}
public void Dispose()
{
_cts?.Cancel();
_processingTask?.Dispose();
_cts?.Dispose();
}
}BackgroundProcessor دارای یک _processingTask و یک _cts (منبع توکن لغو) است. متد StartProcessing یک Task طولانی مدت را آغاز میکند که دادهها را پردازش میکند تا زمانی که لغو درخواست شود. متد Dispose این کلاس، CancellationTokenSource را لغو میکند تا _processingTask متوقف شود، سپس _processingTask را Dispose میکند تا از پاکسازی مناسب هرگونه منبع پنهان اطمینان حاصل شود و در نهایت _cts را نیز Dispose میکند. این سناریو به خوبی نشان میدهد که چگونه Task.Dispose در کنار CancellationToken برای مدیریت کامل چرخه حیات یک Task طولانی مدت و آزاد کردن منابع آن عمل میکند.Dispose در کلاس Task، در اغلب برنامههای مدرن #C به ندرت به فراخوانی صریح آن نیاز است. زمان اجرای داتنت به طور هوشمندانه پاکسازی Task را مدیریت میکند، به خصوص زمانی که Task به درستی await میشود یا نتیجه آن دسترسی پیدا میکند. با این حال، درک زمان و چرایی استفاده از Task.Dispose در سناریوهای خاص، مانند جلوگیری از استثناهای مشاهده نشده یا مدیریت وظایف طولانی مدت که نیاز به پاکسازی منابع پس از اتمام یا لغو دارند، میتواند بسیار مفید باشد.Dispose کردن یک Task، آن را لغو نمیکند؛ برای لغو باید از CancellationToken استفاده کنید. علاوه بر این، در نظر گرفتن مدیریت سراسری استثناهای مشاهده نشده به جای تکیه بر Dispose برای هر Task، یک رویکرد قویتر است. در بیشتر مواقع، بهتر است اجازه دهید زمان اجرای داتنت پاکسازی Task را انجام دهد، اما دانستن زمان و نحوه استفاده از Task.Dispose یک ابزار ارزشمند در جعبه ابزار هر توسعهدهنده .NET است.