C# enthusiast. NET foundation member
اشتباهات رایج در استفاده از Async/Await + راه حل
در این مقاله قصد داریم به 7 اشتباه رایج در استفاده از Async/Await بپردازیم و راه حل مناسب را برای آن ارائه دهیم.
در این مقاله از Github Gist استفاده شده است و لود شدن بخش مربوط به کدها ممکن است کمی زمانبر باشد. همچنین ممکن است که نیازمند نرم افزار رفع تحریم باشید.
1-یک بار async. همواره async
یکی از اشتباهات رایج هنگام استفاده از TPL این است که یک Task که async میباشد در یک متد sync صدا زده شود. همیشه به یاد داشته باشید که اگر یکجا از متد async استفاده کرده اید همواره آن را در متد های async استفاده کنید و برای متد های sync، معادل async آن را بنویسید.
در مثال زیر یک کلاس SomeTask داریم که یک متد async دارد.
حال در کلاس Main آن را به صورت زیر در متد Main استفاده میکنیم که اشتباه است.چون یک متد async را داخل یک متد sync صدا زده ایم.
متد Main را به شکل زیر به صورت زیر بازنویسی میکنیم و معادل async آن را مینویسیم.
2-استفاده از async void
یکی دیگر از اشتباهات بسیار خطرناک در مورد استفاده از async/await استفاده از async void به جای async Task می باشد. متدی که async void باشد دو مشکل جدی را به همراه خود دارد:
1- در سایر متد ها نمی توان آن را await کرد چرا که خروجی آن از جنس Task نیست و در نتیجه ممکن است که جوابی که مد نظر ماست از این متد دریافت نشود.
2- در صورت وقوع exception نمیتوان آن را catch کرد چون که خروجی از جنس void می باشد و هیچ مقداری ندارد. و در نتیجه کل برنامه crash میکند.
پس بهتر است که جنس خروجی متد های async را از نوع Task قرار دهیم.
نکته:تنها یک جا و فقط یک جا مجاز به استفاده از async void هستیم و آن هم موقع هندل کردن Event ها می باشد.
در کلاس SomeTask زیر متد DoSomeWork به صورت async void نوشته شده است.
اگر دقت کنید در متد Main کلاس Program وقتی که متد DoSomeWork را میخواهیم await کنیم به خطا بر میخوریم.
برای بر طرف کردن خطا Signature متد را به جای void به Task بر میگردانیم.
3-استفاده از Task.Run به جای Task.FromResult
همیشه بهتر است وقتی که متد شما async است ولی بدنه آن هیچ عملیات async ای ندارد و نمیخواهید که عملیاتی را به صورت async انجام دهید از Task.FromResult به جای Task.Run استفاده کنید . چرا که استفاده از Task.Run هیچ مزیتی به همراه خود ندارد و تنها یک Thread اضافی از Thread Pool را مصرف میکند(مگر آنکه واقعا نیاز داشته باشید که عملیات در یک Thread دیگر انجام شود که مانعی ندارد).
مجددا در متد DoSomeWork اینبار یک مقدار int را بر میگرداند. در روش اول این کار را با استفاده از Task.Run انجام میدهیم که اشتباه است.
در روش دوم همانطور که گفته شد به جای استفاده از Task.Run از Task.FromResult و یا Task.CompletedTask برای نوع غیر جنریک Task استفاده میکنیم.
4-استفاده از .Result و .Wait
یکی دیگر از اشتباهات رایج، استفاده از Result و Wait می باشد. با اینکار عملیات async به صورت sync اجرا میشود و همچنین یک Thread دیگر برای اجرای عملیات استفاده میشود. یعنی عملا دو Thread برای اجرا به صورت sync مورد استفاده قرار میگیرد. این کار ممکن است که موجب ایجاد بن بست (Deadlock) شود. در این مورد نیز باید قانون "یک بار async همواره async" را رعایت کنیم و متد های async را همواره به صورت async اجرا کنیم. در نظر داشته باشید که در اکثر مواقع میتوان معادل async تمام متد های sync را نوشت.
در متد DoSomeWork زیر خروجی آن به صورت Task می باشد.
در متد Main زیر برای بدست آوردن نتیجه Task از Result آن استفاده کرده ایم که کار اشتباهی است.
روش درست آن است که متد main را به صورت async در بیاوریم و برای بدست آوردن نتیجه آن را await کنیم. پس متد main را به صورت زیر بازسازی میکنیم.
5- عدم استفاده از ()GetAwaiter().GetResult
زمانی که نیاز است حتما یک متد async را داخل یک متد sync صدا بزنید، بهتر است که به جای استفاده از .Wait و .Result از ()GetAwaiter().GetResult استفاده کنید. با این کار در صورت وقوع exception به جای برگرداندن Aggregate Exception که مجموعه ای از Exception ها است ، خود Exception را بر میگرداند.
6-استفاده بیش از حد از async await
یکی از اشتباهات رایج استفاده بیش از حد از async await می باشد. در اینجا میتوانیم از تکنیک Task Eliding استفاده کنیم. یعنی به جای اینکه یک Task را await کنیم، خود Task را مستقیما بعنوان خروجی متد برگردانیم و در متد های بالاتر از آن هنگامی که میخواهیم از آن استفاده کنیم، آن را await کنیم.
در متد GetGoogleContent زیر یک Task از نوع stringداریم که در آن نتیجه کار await شده است و محتوای html صفحه اصلی Google برگردانده شده است.
به جای اینکار، بهتر است که مستقیما خود Task را برگردانیم و در متد Main آن را await کنیم.
سپس از این متد در متد main به شکل زیر استفاده میکنیم.
7-سینک کردن یک متد async در Constructor یک کلاس
همانطور که میدانید سازنده یک کلاس نمیتواند async باشد. اگر نیاز داشته باشیم که یک async call در constructor یک کلاس داشته باشیم، باید از .Wait و یا .Result استفاده کنیم که قبلا به عنوان یک اشتباه رایج به آن اشاره کرده ایم.
یک راه حل مناسب این است که از الگوی Factory Method استفاده کنیم. به جای اینکه از Constructor برای ساخت کلاس استفاده کنیم، از یک static method برای ایجاد آن استفاده کنیم.
در مثال زیر فرض کنید که کلاس SomeTask برای ایجاد نیاز به یک async call داشته باشد. پس این کلاس به شکل زیر خواهد بود.
و سپس در متد Main به شکل زیر از کلاس SomeTask استفاده خواهیم کرد.
برای حذف .Wait مراحل زیر را انجام میدهیم.
1-ابتدا Constructor کلاس را به شکل private تبدیل میکنیم.
2- سپس یک متد استاتیک به نام Create که حاوی امضای async می باشد را ایجاد کرده و کلاس را داخل آن New میکنیم.
پس کد کلاس SomeTask را به شکل زیر تغییر میدهیم.
سپس برای استفاده از کلاس SomeTask در متد main، کد کلاس main را به شکل زیر تغییر میدهیم.
نتیجه گیری
در این مقاله به بررسی 7 اشتباه رایج هنگام استفاده از async/await پرداختیم.به طور کلی توصیه میشود که هر جا توانستید از async/await استفاده کنید و سعی کنید که Blocking Call نداشته باشید و اگر نیاز به نتیجه یک async call داشتید، آن را await کنید. همچنین همواره در async call ها Task و یا نوع جنریک آن Task<> را استفاده کنید.
مقالات بیشتر در دات نت زوم
مطلبی دیگر از این انتشارات
دات نت 5 و 5 ASPNET Core و EFCore 5 پیش نمایش 3 منتشر شد
مطلبی دیگر از این انتشارات
C# 9.0: init-only - ایجاد خصوصیات تغییر ناپذیر بدون سازنده
مطلبی دیگر از این انتشارات
تفاوت Re-execute و Redirect در asp.net core