Farhad Nosrati فرهاد نصرتی
Farhad Nosrati فرهاد نصرتی
خواندن ۸ دقیقه·۳ سال پیش

Asynchronous programming

قبل شروع یک rollback بزنیم به Synchronous programming .
در این نوع programming برنامه Sequential اجرا میشه یعنی ؟
نرم افزار بر روی یک thread اجرا شده و clr شروع میکند خط به خط کد ها را اجرا کردن و اگر خط قبلی اجرا نشود هیچوقت خط بعدی اجرا نخواهد شد .
اشکال : اگر اجرای یک خط کد ۱۰ ثانیه زمان ببرد ، چون عملیات فوق (عملیات ۱۰ ثانیه ) بر روی thread اصلی نرم افزار ( main thread ) اجرا شده است ، نرم افزار تا اتمام آن خط در همان حالت و وضعیت منتظر میماند تا ۱۰ ثانیه به پایان برسد بعد وارد خط بعدی شود .
مثلا فرض کنید اگر ما یک عملیات زمانبری داشته باشیم مثل گذارش گیری ماهانه یا قرار باشد همزمان علاوه بر گذارش گیری چند عملیات دیگر هم انجام دهیم ، در این شرایط انجام عملیات همزمان ( parallel ) غیر ممکن است چون عملیات گذارشگیری بر روی main thread نرم افزار اجرا شده ، در این شرایط نه تنها عملیات دیگری نمیتواند اجرا شود بلکه کل نرم افزار صبر میکند تا عملیات اول ( گذارشگیری ) به اتمام برسد بعد وارد عملیات بعدی شود .

ما نیاز داریم نرم افزار ، همزمان با گذارشگیری ، عملیات دیگری هم اجرا کند . اینجاست که باید برویم سراغ Asynchronous programming :
در Asynchronous programming نرم افزار ما در شرایط ذکر شده صبر نمیکند و میتواند همزمان با گذارش گیری ، عملیات های دیگری هم اجرا کند .
سوال : چگونه این اتفاق شدنیست ؟

این سوال مربوط به multi threading است ولی در این متن تصميم ندارم وارد مباحثش شوم ولی توضیح مختصری ازش میگویم که گوشیه زهنمان باشد .

سی پی یو cpu چند هسته ای هست و هر هسته از چندین thread تشکیل شده که ما میتونیم برای این که سرعت و کارایی نرم افزارمان را بالا ببریم و از تمام قدرت cpu برای اجرای نرم افزارمان استفاده کنیم .

چون یک thread نمیتواند بیشتر از یک process را هندل کند ، میتوانیم جدا از main thread ، از thread های دیگر cpu به طور مستقل از هم و همزمان استفاده‌ کنیم .

نکته : در حالت multi threading نباید چند عملیات که همزمان اجرا میشوند به هم وابسته باشند چون اگر اجرا شدن متدی ، وابسته به نتیجه ( return ) متد دیگر باشد ، خطا رخ میدهد و برای این نوع عملیات حتما باید صبر کرد .

کاربرد اصلی Asynchronous این است که نرم افزار منتظر نمیماند به این شکل که main thread نرم افزار را آزاد میکنیم و برای هر عملیات یک thread دیگر و مستقل تخصیص میدهیم .

دو کلمه کلیدی Async awayt :
Async :
این کلمه کلیدی در ابتدای متد اعلام میکرد که متد جاری یک متد از نوع Asynchronous است ‌.
توجه : در اتمام نام متد های Asynchronous حتما باید با Async پایان یابد که زمان خوانده نام متد ، مشخص شود این متد یک متد Asynchronous است .
مثل نام متد زیر :

GetCarNameAsync();

وقتی متدی از نوع Async تعریف می شود باید از یک سری قوانین تبعیت کند :

متدی که از نوع Async است حتما باید یک Task را اجرا کند و خروجی از نوع Task برگرداند که میتواند یک Task از نوع T باشد .
به شکل زیر :

Task
Task<T> ==> Task<int> , Task<string>
void

یک task میتواند اجرای یک یا چند thread  باشد که وقتی یک متد  async اجرا میشود در واقع clr آن را در یک thread مستقل و همزمان با thread های دیگر و در background اجرا میکند .

نکته:  توجه داشته باشید که یک thread  نمیتواند مقدار بازگشتی داشته باشد ولی Task میتوان و یک thread میتواند فقط یک process رو هندل کند ولی Task چندیدن کار را میتوند انجام دهد .

البته همیشه و حتما معنای task در ابتدای متد ها به معنای run شدن آن متد در یک thread جدید نیست که جلوتر توضیحات کامل ارائه میشود .

Await :

اگر متدی در یک thread مستقل اجرا شود در واقع ، وقتی کامپایلر به بلاک آخر متد برسد آن thread بسته میشود و هیچ وقت از وضعیات عملیاش و در صورت بروز هر گونه خطا در زمان اجرا باخبر نخواهیم شد پس حتما برای متد های Async task صبر کنیم مگر در شرایط خواص .

برای این کارمیتوانیم از کلمه کلیدی  await استفاده کنیم .

این کلمه کلیدی این امکان را میدهد که ما در thread اجرا شده صبر کنیم و تا به پایان نرسیدن عملیات ،  thread بسته نشود .

به مثال زیر توجه کنید :


static async Task Main(string[] args)
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("Main:" + threadId);
            await GettaskAsync();
            threadId = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("Main:" + threadId);
            Console.ReadKey();
        }
        public static async Task GetTaskAsync()
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("GettaskAsync:" + threadId);
            await Task.Delay(1000);
            threadId = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine("GettaskAsync:" + threadId);
        }

شرح کد : متدی از نوع async به نام Main بر روی thread اصلی نرم افزارمان اجرا است ، وقتی کامپایلر بر روی await داخل متد Main میرسد ، با همان thread جاری وارد متد async فراخوان شده ( GetTaskAsync ) میشود و شروع به پردازش متد دوم میکند ، حال اگر در بدن متد فراخوان شده await داشته باشیم و به آن برسد ، در صورت این که پردازش آن Await زمان بر باشد و باعث معلق شدن نرم افزار یا thread اصلی شود ، در این لحضه thread اصلی ( جاری ) را آزاد کرده و یک thread جدید اجرا میکند و به صورت yield تا پایان عملیات به متد فراخوانی شده ما گوش میدهد و در زمان پایان عملیات thread جدید ، ادامه عملیات خود را با همان thread جدید ساخته شده ادامه داده و به متد اول باز می گردد ، که این کار یک روش . net ، برای جلوگیری از thread deadlock است .
حال اگر در متد دوم ( thread دوم ) هم یک await باشد ، عملیات بالا را تکرار میکند .

در این شرایط نرم افزار هنگ نمیکند چون این روش یک multi tasking بود و در دات نت ، async یک signature برای پیاده سازی multi tasking است .

قطعا برنامه نویسی muti threading پیچیده است به همین دلیل .net با روش TAP این امکان را به ما میدهد که با استفاده از کلمات کلیدی async/await/Task کد های مشابه sync بنویسیم و کامپایلر زحمت تغییرش به multi threading برایمان بکشد .

قوانین استفاده از async/await/Task :

یک بار async و همواره async :
• هیچوقت یک متد async را در یک متد sync فراخوان نکنید . برای متد های sync متد معادل sync توسعه دهید و برای متد های async متد های معادل async توسعه دهید و استفاده کنید .

• هیچوقت از دستور .result و یا .wait استفاده نکنیند چرا که این کار باعث بروز deadlock شود .
به کد زیر توجه کنید :

public string GetCarAsync()
        {
            return GetCarNameAsync():
        }
        public async Task<string> GetCarNameAsync()
        {
            await Task.Run(() => Task.Delay(1000));
            return "praid";
        }

شرح :امکان return نتیجه متد async در یک متد sync وجود ندارد چون متد async یک Task برمیگرداند .
متن خطای کد بالا "امکان تغییر string یه یک Task<straing> به روش implicit وجود ندارد" .
پس برای این که بتوانیم صورت مسئله را پاک کنیم ، از دستور .result در انتهای فراخوانی اضافه میکنیم تا مقدار result (که مقدار string باشد) را از Task گرفته ( جداکرده ) و نتیجه یک string بازگرداند .

return GetCarNameAsync().result;


علت خطا : این کار باعث اجرا شدن عملیات async به روش sync میشود ولی همواره دو thread درگیر هندل کردن یک عملیات sync هستند ، در این شرایط دات نت thread را lock کرده و deadlock رخ میدهد .

پیشنهاد : استفاده از GetAwaitr().GetResult(); به جای دیتور .result.
در شرایطی که ناچار به استفاده از متد async در متد sync شدید ، به جای دستور .result میتوانید از دستور زیر برای این کار استفاده کنید

return GetCarNameAsync().GetAwaitr().GetResult();

این دستور اجازه میدهد زمان بروز exception در thread ، به جای throw کردن aggregate exception ، میتواند exception را برگرداند .

• دستور  Task.Run .

متد Run به عنوان ورودی یک Delegate از نوع Action میگیرد و بلافاصله یک thread اجرا کرده و delegate دریافت کرده را در thread جدید اجرا می کند . پس به جز مواقع نیاز و ضروری ، از این دستور استفاده نکنیم چون یک thread از thread pool را اشغال میکند .

نکته مهم : در صورت لزوم میتوانید ، به جای Task.Run از دستور Task.FromResult استفاده کنید ، با همان عملکرد و با این تفاوت که thread جدید اجرا نمیکند .

توجه : ما به جای delegate action از lambda expression استفاده کرده ایم .

به متد های زیر توجه کنید :

public async Task<string> GetCarAsync()
        {
            return await GetCarNameAsync();
        }
        public async Task<string> GetCarNameAsync()
        {
await Task.Run(() => Task.Delay(1000));
            return "praid";
        }

نکته : اگر برای دستور Task.Run مانند دستور بالا await استفاده کنید یعنی برایش صبر کنید ( مانند کد بالا) ، clr برای اجرا شدن آن thread ، در همان line صبر میکند و بعد از پایان عملیات به خط بعدی میرود ولی اگر await نداشته باشد ، thread جدید ساخته و همزمان با thread اصلی در background اجرا میشود ، ولی با این تفاوت که clr منتظر پایان این thread نمیماند و بعد از اجرای خط Task.Run به خط بعدی میرود و همزمان thread جدید در backgroun اجرا است .

توجه : اگر متد خروجی نداشته باید فقط Task برمیگرداند پس به ۳ دلیل هیچگاه از دستور async Task void استفاده نکنید .
1 - در متد های async دیگر نمیتوانیم فراخوانی و await کنیم ، چون خروجی آن از نوع void است نه Task ، پس حتما نتیجه بازگشتی آن با Task متفاوت است و احتمال بروز dead lock در عملیات آن وجود دارد .

نکته : استفاده از این روش در شرایط خیلی خواص و با دانش و اگاهی کامل از عملیاتی که در حال توسعه و پیاده سازی ان هستیم بلامانع است .
2- در متد async Task void اگر exception رخ دهد ، نمیتوانید آن را throw catch کنید چون نتیجه متد از نوع void است و مقداری بر نمیگرداند .

دوستان عزیز ، خیلی ممنون که وقت گران قدرتونو گذاشتید و تا به اینجا همراهیم کردید .

اگه اشتباه مفهومی داشتم یا ایراد تایپی داشتم یا ایراد توضیح داشتم خیلی خوشحال میشم بهم بگید تا اصلاح کنم .

مخلصیم ...


asyncawaittaskasynchronoussync
https://github.com/nosratifarhad
شاید از این پست‌ها خوشتان بیاید