اشتباهات رایج در استفاده از Async/Await + راه حل

در این مقاله قصد داریم به 7 اشتباه رایج در استفاده از Async/Await بپردازیم و راه حل مناسب را برای آن ارائه دهیم.




در این مقاله از Github Gist استفاده شده است و لود شدن بخش مربوط به کدها ممکن است کمی زمانبر باشد. همچنین ممکن است که نیازمند نرم افزار رفع تحریم باشید.


1-یک بار async. همواره async

یکی از اشتباهات رایج هنگام استفاده از TPL این است که یک Task که async میباشد در یک متد sync صدا زده شود. همیشه به یاد داشته باشید که اگر یکجا از متد async استفاده کرده اید همواره آن را در متد های async استفاده کنید و برای متد های sync، معادل async آن را بنویسید.

در مثال زیر یک کلاس SomeTask داریم که یک متد async دارد.

https://gist.github.com/babaktaremi/ce5e78352a4969983337dd04fe991098

حال در کلاس Main آن را به صورت زیر در متد Main استفاده میکنیم که اشتباه است.چون یک متد async را داخل یک متد sync صدا زده ایم.

https://gist.github.com/babaktaremi/61ce26657a1447948f5b4cdc6db2e61c

متد Main را به شکل زیر به صورت زیر بازنویسی میکنیم و معادل async آن را مینویسیم.

https://gist.github.com/babaktaremi/ebb452cbb21d66570a6e5955b745ec2c

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 نوشته شده است.

https://gist.github.com/babaktaremi/fd627e4c914849f4c07be0b35abb830c

اگر دقت کنید در متد Main کلاس Program وقتی که متد DoSomeWork را میخواهیم await کنیم به خطا بر میخوریم.

https://gist.github.com/babaktaremi/f4efd37a29b3b1f2492ebb2ec47dc376

برای بر طرف کردن خطا Signature متد را به جای void به Task بر میگردانیم.

https://gist.github.com/babaktaremi/fb05176a5987de40179cc3bcadcc6d59

3-استفاده از Task.Run به جای Task.FromResult

همیشه بهتر است وقتی که متد شما async است ولی بدنه آن هیچ عملیات async ای ندارد و نمیخواهید که عملیاتی را به صورت async انجام دهید از Task.FromResult به جای Task.Run استفاده کنید . چرا که استفاده از Task.Run هیچ مزیتی به همراه خود ندارد و تنها یک Thread اضافی از Thread Pool را مصرف میکند(مگر آنکه واقعا نیاز داشته باشید که عملیات در یک Thread دیگر انجام شود که مانعی ندارد).

مجددا در متد DoSomeWork اینبار یک مقدار int را بر میگرداند. در روش اول این کار را با استفاده از Task.Run انجام میدهیم که اشتباه است.

https://gist.github.com/babaktaremi/daf20daf26c16efc1660c13e3e6e5b3e

در روش دوم همانطور که گفته شد به جای استفاده از Task.Run از Task.FromResult و یا Task.CompletedTask برای نوع غیر جنریک Task استفاده میکنیم.

https://gist.github.com/babaktaremi/f3648807ff1c9c6154e24e507d020d61


4-استفاده از .Result و .Wait

یکی دیگر از اشتباهات رایج، استفاده از Result و Wait می باشد. با اینکار عملیات async به صورت sync اجرا میشود و همچنین یک Thread دیگر برای اجرای عملیات استفاده میشود. یعنی عملا دو Thread برای اجرا به صورت sync مورد استفاده قرار میگیرد. این کار ممکن است که موجب ایجاد بن بست (Deadlock) شود. در این مورد نیز باید قانون "یک بار async همواره async" را رعایت کنیم و متد های async را همواره به صورت async اجرا کنیم. در نظر داشته باشید که در اکثر مواقع میتوان معادل async تمام متد های sync را نوشت.

در متد DoSomeWork زیر خروجی آن به صورت Task می باشد.

https://gist.github.com/babaktaremi/05dea35b7cdc9eca744c5487ba57bd9d

در متد Main زیر برای بدست آوردن نتیجه Task از Result آن استفاده کرده ایم که کار اشتباهی است.

https://gist.github.com/babaktaremi/0cdcfcada14da0fd60fb61741a138959

روش درست آن است که متد main را به صورت async در بیاوریم و برای بدست آوردن نتیجه آن را await کنیم. پس متد main را به صورت زیر بازسازی میکنیم.

https://gist.github.com/babaktaremi/c212957c92c58fe3cb7e9079598074de

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 برگردانده شده است.

https://gist.github.com/babaktaremi/47bcc7ff54fd6ddbd36c319a7630c822


به جای اینکار، بهتر است که مستقیما خود Task را برگردانیم و در متد Main آن را await کنیم.

https://gist.github.com/babaktaremi/0b10b5a3f8cb2f147fd64f2e3001d70b


سپس از این متد در متد main به شکل زیر استفاده میکنیم.

https://gist.github.com/babaktaremi/6e83827ef1e429f0fa82f798976e4f26


7-سینک کردن یک متد async در Constructor یک کلاس

همانطور که میدانید سازنده یک کلاس نمیتواند async باشد. اگر نیاز داشته باشیم که یک async call در constructor یک کلاس داشته باشیم، باید از .Wait و یا .Result استفاده کنیم که قبلا به عنوان یک اشتباه رایج به آن اشاره کرده ایم.

یک راه حل مناسب این است که از الگوی Factory Method استفاده کنیم. به جای اینکه از Constructor برای ساخت کلاس استفاده کنیم، از یک static method برای ایجاد آن استفاده کنیم.

در مثال زیر فرض کنید که کلاس SomeTask برای ایجاد نیاز به یک async call داشته باشد. پس این کلاس به شکل زیر خواهد بود.

https://gist.github.com/babaktaremi/253343ee36c546d91414e8efa6d780cc

و سپس در متد Main به شکل زیر از کلاس SomeTask استفاده خواهیم کرد.

https://gist.github.com/babaktaremi/f68d10354619bc10865ea6990939cbd8


برای حذف .Wait مراحل زیر را انجام میدهیم.

1-ابتدا Constructor کلاس را به شکل private تبدیل میکنیم.

2- سپس یک متد استاتیک به نام Create که حاوی امضای async می باشد را ایجاد کرده و کلاس را داخل آن New میکنیم.

پس کد کلاس SomeTask را به شکل زیر تغییر میدهیم.

https://gist.github.com/babaktaremi/446f7f4d3788e47e0c6fc2dbe2c3632d

سپس برای استفاده از کلاس SomeTask در متد main، کد کلاس main را به شکل زیر تغییر میدهیم.

https://gist.github.com/babaktaremi/31fb32edecbb327f7b8f2a8583a6ec70


نتیجه گیری

در این مقاله به بررسی 7 اشتباه رایج هنگام استفاده از async/await پرداختیم.به طور کلی توصیه میشود که هر جا توانستید از async/await استفاده کنید و سعی کنید که Blocking Call نداشته باشید و اگر نیاز به نتیجه یک async call داشتید، آن را await کنید. همچنین همواره در async call ها Task و یا نوع جنریک آن Task<> را استفاده کنید.


مقالات بیشتر در دات نت زوم

https://t.me/DotNetZoom