Amir Mokarchi
Amir Mokarchi
خواندن ۶ دقیقه·۴ سال پیش

refactor یک برنامه sync به async در ASP.NET - بخش دوم

در این پست ، فرایند refactoring یک برنامه وب همزمان(synchronous) ASP.NET را به یک برنامه غیر همزمان (asynchronous) شروع می کنیم و نشان می دهیم چه نوع مشکلی ممکن است درانجام این کار بوجود آید.

برنامه وب که می خواهیم آنرا refactor کنیم یک برنامه نسبتاً ساده است. این سیستم یک سرویس خارجی را فراخوانی می کند تا داده های برگشتی توسط این سرویس را نمایش دهد.

داده هایی که بازگشت داده می شود مربوط به یکی از چهار objects زیر است:

Users, Posts, Albums, Photos

اگر بخش اول این مطلب رو مطالعه کرده باشید شما می دانید که برای اینکه یک برنامه synchronous را asynchronous کنیم و همه چیز را خراب نکنیم ، به طور کلی می خواهیم از پایین ترین سطح سلسله مراتب داده شروع کنیم که در اینجا مقصود object تصویر می باشد.

بنابراین ، بدون هیچ حرف بیشتری ، بیایید شی فوق رو refactor کنیم!

Refactoring the PhotoRepository and PhotoController

کد فوق مربوط است برای PhotoRepository :

من می‌خواهم تا آنجا که ممکن است کمترین کار ممکن را انجام دهم، بنابراین بلوک های کد را با استفاده از متد Task.Run به صورت asynchronous میکنم. من تصمیم گرفتم که فقط این کار را انجام دهم:

نکته جالب در مورد این راه حل این است که من حتی نیازی به تغییر اینترفیس IPhotoRepository نداشتم ، بنابراین کار بسیار کمی انجام شد. و اگر کد را اجرا کنیم ، می بینیم که به طرز شگفت انگیزی ، در واقع کار می کند!

Problem #1: Async-Over-Sync

آنچه ما انجام داده ایم یک ضد الگویی است که به عنوان async-over-sync شناخته شده است

هنگام اجرای این متد:

  • 1- یک thread برای اجرای GetAll فراخوانده می‌شود.
  • 2- یک thread کاملا متفاوت برای اجرای محتوای Task.Run فراخوانده می‌شود.
  • 3- در این مرحله، thread اصلی بلاک شده و منتظر میماند که photoTask.Result نتیجه را بازگرداند.

سیستم‌های ما اکنون قادر به رسیدگی به درخواست‌های همزمان کمتری نسبت به یک سیستم synchronous می باشد.

بنابراین اکنون این سؤال مطرح می شود: چگونه این مسئله را برطرف کنیم؟ برای پاسخ به آن ، باید دو مشکل را حل کنیم.

Finishing the PhotoRepository

اولین مشکل کلاس WebClient است. یک جانشین جدید و مناسب تر برای فراخوانی API هایی که ما انجام می دهیم کلاس HttpClient است. من به عنوان مالک پروژه می خواهم کل پروژه خود را از استفاده از WebClient به HttpClient تغییر دهم. با این حال، زمانی که سعی می‌کنم این کار را انجام دهم، فورا به یک مشکل می‌رسم:

HttpClient does not expose synchronous methods!

مشکل دوم این است که ، همانطور که در بالا دیدیم ، wrap کردن کد synchronous در Task ایده بدی است. بنابراین در حال حاضر من "مجبور" شده ام تا دوباره refactor به سمت برنامه‌نویسی asynchronous انجام دهم.

بنابراین در اینجا کاری که می خواهم انجام دهم: من فقط قصد دارم تا Repository را به asynchronous تبدیل(refactor) کنم و نه PhotoController که درسطح بالاترقرار دارد.

PhotoRepository

PhotoController

به استفاده از .Result توجه کنید. معنی این امر این است که وقتی کنترلر repository را صدا می کند ، منتظر می ماند تا نتیجه متد برگردد ، سپس از آن نتیجه استفاده می کند. اما آیا واقعاً این اتفاق می افتد؟
نه. آنچه واقعاً اتفاق می افتد بسیار بدتر است ، و کاملاً واضح نیست.

Problem #2: Deadlock

به دلایلی که احتمالاً در حال حاضر مشخص نیست ، کد بالا منجر به deadlock خواهد شد. برای فهمیدن دلیل آن ، ابتدا باید چیزی را بدانیم که در هنگام فراخوان یک asynchronous task در پشت صحنه اتفاق می افتد.

هر زمان که یک asynchronous task ایجاد شود ، اگر thread اجرای آن کار باید منتظر بماند تا اتفاقی بیفتد ، می‌تواند محیط اجرایی را ترک کند و کار دیگری انجام دهد

اما قبل از این که آن را ترک کند، چیزی به نام SynchronizationContext ایجاد می‌کند، که context پیرامون اجرای task را ذخیره می‌کند و برای از سرگیری اجرای این برنامه در آینده مورد نیاز است.

توجه: پاراگراف فوق فقط مربوط به ASP.NET Framework است ، نه ASP.NET Core.

می‌دانم که این تصویر خیلی شلوغ و گیج‌کننده است، اما خوشبختانه یک دقیقه طول می‌کشد. برای توضیح دادن که چرا deadlock در حال رخ دادن است:

  • 1- اکشن کنترلر Index متد GetAll را از Repository فراخوانی می کند
  • 2- GetAll درخواست async برای GetStringAsync را شروع می کند
  • 3- GetStringAsync یک task uncompleted را باز می‌گرداند.
  • 4- اکنون منتظر نتیجهGetStringAsync هستیم. Context در این مرحله گرفته می شود و در آینده برای بازگرداندن task استفاده می شود.
  • 5- اکنون متد controller به طور همزمان result از GetAll را مسدود می کند که این امر مانع احیای Context می‌شود.
  • 6- از طرفی دیگر ، GetStringAsync پایان می یابد. این کار سبب می شود GetStringAsync پیام completes را برگشت دهد
  • 7- اکنون این task برای اجرا آماده است، اما باید منتظر بماند تا Context در دسترس باشد تا این کار را انجام دهد.
  • 8- به دلیل 5 و 7 ، هیچگاه Context فراهم نمی شود ، بنابراین ما DeadLock داریم.

به عبارتی، کدهای async منتظر پایان کار Result هستند تا نتیجه را بازگردانند. در همین لحظه کدهای همزمان برنامه نیز منتظر کدهای async هستند تا خاتمه یابند. نتیجه‌ی کار یک deadlock است.

بنابراین ما در این باره چه کار کنیم؟ شما ممکن است از پست قبلی به خاطر داشته باشید که async تمایل دارد مثل یک ویروس پخش شود. راه‌حل در اینجا این است که اجازه دهید این کار را انجام دهد وکنترلر را refactor کنید تا از async / await استفاده کند.

مخزن سورس پروژه فوق و همچنین مطالب بررسی شده در مقاله فوق در این لینک قابل دسترس می باشد

refactorsyncasyncASP.NETawait
DotNet Developer
شاید از این پست‌ها خوشتان بیاید