Runtime Async در داتنت ۱۱: تحولی بنیادین در برنامهنویسی غیرهمگام (Async)
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۱۱/۲۶ ۰۹:۱۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
async/await در سیشارپ ۵، نحوه مدیریت کدهای غیرهمگام (Asynchronous) بر دوش کامپایلر (Roslyn) بوده است. اما با عرضه .NET 11 Preview 1، شاهد یک تغییر پارادایم بزرگ هستیم: Runtime Async. در این رویکرد جدید، به جای اینکه کامپایلر مسئول بازنویسی کدهای شما باشد، خودِ Runtime (محیط اجرا) داتنت به صورت بومی (Native) مفهوم متدهای async را درک و مدیریت میکند. در این مقاله بررسی میکنیم که Runtime Async چیست، چرا اهمیت دارد و چگونه میتوانید همین امروز در داتنت ۱۱ با آن کار کنید.async مینوشتید، کامپایلر سیشارپ در پشت صحنه آن را کاملاً تغییر میداد و به یک ماشین وضعیت (State Machine) تبدیل میکرد. این ماشین وضعیت، یک struct است که پیادهسازی IAsyncStateMachine را بر عهده دارد و وظیفهاش حفظ وضعیت متد در نقاط توقف (await) است.MoveNext میشود و دنبال کردن جریان اصلی کد دشوار است.await یک چرخه کامل توقف/بازگشت (Suspend/Resume) را تحمیل میکند.[MethodImpl(MethodImplOptions.Async)] نشانهگذاری شده است.await زنده میمانند، در حافظه ذخیره (Hoist) میشوند.Task را به کلی حذف کند.AsyncHelpers برای اعلام تعلیق استفاده میکنند. وقتی کامپایلر کد شما را ترجمه میکند، به جای ساخت ماشین وضعیت، فراخوانیهایی به AsyncHelpers.Await(...) درج میکند. Runtime این فراخوانیها را رهگیری کرده و اگر Task هنوز کامل نشده باشد، متد را به بهینهترین شکل ممکن معلق (Suspend) میکند.namespace System.Runtime.CompilerServices
{
public static class AsyncHelpers
{
// نمونهای از متدهای کمکی که Runtime از آنها استفاده میکند
[MethodImpl(MethodImplOptions.Async)]
public static void Await(Task task);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(Task<T> task);
// پشتیبانی از ValueTask و سایر Awaitableها نیز وجود دارد
}
}DOTNET_RuntimeAsync=1 نیست. Runtime به صورت پیشفرض آماده اجرای این متدهاست.runtime-async کامپایل میشوند، اکنون میتوانند به صورت Ahead-of-Time (AOT) کامپایل شوند. این یعنی برنامههایی بسیار سبکتر و سریعتر که از مزایای Runtime Async نیز بهره میبرند..csproj خود را باز کنید و تارگت را تغییر دهید:<TargetFramework>net11.0</TargetFramework>
<PropertyGroup>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<Features>$(Features);runtime-async=on</Features>
</PropertyGroup>async Task<string> FetchDataAsync(HttpClient client, string url)
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}[MethodImpl(MethodImplOptions.Async)] // مشخصه جدید
internal static Task<string> FetchDataAsync(HttpClient client, string url)
{
// استفاده مستقیم از AsyncHelpers بدون ماشین وضعیت
HttpResponseMessage response = AsyncHelpers.Await<HttpResponseMessage>(client.GetAsync(url));
response.EnsureSuccessStatusCode();
return AsyncHelpers.Await<string>(response.Content.ReadAsStringAsync());
}stateMachine پیچیده ساخته میشود که شامل فیلدهای state، builder و awaiter است. متد اصلی فقط این ماشین را میسازد و استارت میزند. منطق اصلی در یک متد MoveNext طولانی و پیچیده با چندین دستور goto و try/catch پیادهسازی میشود که خوانایی و دیباگ را دشوار میکند.runtime-async مجدداً کامپایل شوند. تا آن زمان، کد شما سریع است اما وقتی وارد متدهای فریمورک میشوید، به مدل قدیمی برمیگردد.async دچار خطا میشود:await MyTask();
async Task<string> MyTask()
{
await Task.Delay(1);
throw new InvalidOperationException("This is an exception.");
return "";
}Unhandled exception. System.InvalidOperationException: This is an exception. at Program.MyTask() in .../Program.cs:line 13 at System.Runtime.CompilerServices.AsyncHelpers.RuntimeAsyncTask`1.DispatchContinuations() --- End of stack trace from previous location --- at Program.<Main>()
DispatchContinuations است. این نشان میدهد که Runtime مستقیماً مسئولیت "توزیع" و مدیریت ادامه اجرای کد (Continuation) پس از await را بر عهده گرفته است.Unhandled exception. System.InvalidOperationException: This is an exception. at Program.MyTask() in .../Program.cs:line 13 at Program.Main() in .../Program.cs:line 7 at Program.<Main>()
AsyncHelpers) را در Stack Trace نشان میدهد. هرچند این ممکن است در نگاه اول کمی متفاوت به نظر برسد، اما هدف نهایی این است که با حذف لایههای اضافیِ ماشین وضعیت، دیباگ کردن برای توسعهدهنده شفافتر شود. البته انتظار میرود در نسخههای نهایی، نمایش این اطلاعات باز هم بهینهتر شود.SynchronizationContext! همان دوست (و گاهی دشمن) قدیمی که با ConfigureAwait(true) (یا به صورت پیشفرض) همراهمان است! وظیفهی اصلی SynchronizationContext این است که "زمینه" فعلی اجرا (مثلاً ترد UI در برنامههای WPF یا WinForms) را حفظ کند و مطمئن شود که پس از پایان عملیات await، ادامهی کد دقیقاً روی همان زمینه اجرا میشود.SynchronizationContext را ضبط (Capture) کند و پس از اتمام کار، ادامه متد را روی آن Post کند.SynchronizationContext نادیده گرفته شده یا حذف شود (Drop).AsyncLocal، ExecutionContext و SynchronizationContext دقیقاً چه رفتاری خواهند داشت. اما این ناحیهای است که نیاز به مراقبت ویژه دارد؛ زیرا اگر Runtime این زمینهها را به درستی منتقل نکند، کدهای وابسته به ترد UI یا دادههای محلی (Local Data) ممکن است دچار مشکل شوند.runtime-async=on است. این تغییر، گامی بزرگ به سوی حذف ماشینهای حالت (State Machines) تولیدشده توسط کامپایلر و استفاده از قابلیت async فراهمشده توسط خود runtime محسوب میشود. این بهروزرسانی نه تنها عملکرد و اندازهٔ کتابخانهها را بهبود میبخشد، بلکه زمینه را برای مهاجرت آسانتر برنامهها به مدل جدید فراهم میکند. در ادامه به بررسی دقیق این تغییرات و ویژگیهای همراه آن میپردازیم.runtime-async برای تمام کتابخانههای runtime:[RequiresPreviewFeatures] برای فعالسازی runtime-async را حذف کرد.Task برمیگرداند و در کلاس مشتقشده Task برمیگرداند، وجود داشت. این تفاوت در نوع بازگشت، باعث مشکل در فراخوانی مجازی (Virtual Dispatch) میشد.Task و Task به درستی کار خواهد کرد.await، بهطور کامل و end-to-end پشتیبانی میشود (Pull Request #125472).