علی قدرتی
علی قدرتی
خواندن ۱۸ دقیقه·۴ سال پیش

برنامه نویسی Synchronous و Asynchronous

برنامه نویسی Synchronous

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

برنامه نویسی Asynchronous

برنامه نویسی ناهمزمان یا ناهمگام هم گفته میشود. در این برنامه نویسی عملیات های فراخوانی شده در لحظه فراخوانی شروع به اجرا و اتمام آنها با توجه به پردازشی که انجام میدهند متغیر است. و ممکن است ترتیب اتمام با ترتیب شروع اجرا یکسان نباشد.



برای توضیح بهتر این قسمت آماده شدن یک صبحانه رو تصور کنید که شامل قسمت های زیر باشد:

  • ریختن یک فنجان قهوه.
  • یک ماهی تابه رو گرم و دو تخم مرغ را بپزید.
  • سه برش بیکن را سرخ کنید.
  • دو تکه نان تست کنید.
  • کره و مربا را به نان تست اضافه کنید.
  • یک لیوان آب پرتقال بریزید.

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

کد این قسمت رو به صورت همزمان ببینیم:

using System; using System.Threading.Tasks; namespace AsyncBreakfast { class Program { static void Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine(&quotcoffee is ready&quot); Egg eggs = FryEggs(2); Console.WriteLine(&quoteggs are ready&quot); Bacon bacon = FryBacon(3); Console.WriteLine(&quotbacon is ready&quot); Toast toast = ToastBread(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine(&quottoast is ready&quot); Juice oj = PourOJ(); Console.WriteLine(&quotoj is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot); } private static Juice PourOJ() { Console.WriteLine(&quotPouring orange juice&quot); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine(&quotPutting jam on the toast&quot); private static void ApplyButter(Toast toast) => Console.WriteLine(&quotPutting butter on the toast&quot); private static Toast ToastBread(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine(&quotPutting a slice of bread in the toaster&quot); } Console.WriteLine(&quotStart toasting...&quot); Task.Delay(3000).Wait(); Console.WriteLine(&quotRemove toast from toaster&quot); return new Toast(); } private static Bacon FryBacon(int slices) { Console.WriteLine($&quotputting {slices} slices of bacon in the pan&quot); Console.WriteLine(&quotcooking first side of bacon...&quot); Task.Delay(3000).Wait(); for (int slice = 0; slice < slices; slice++) { Console.WriteLine(&quotflipping a slice of bacon&quot); } Console.WriteLine(&quotcooking the second side of bacon...&quot); Task.Delay(3000).Wait(); Console.WriteLine(&quotPut bacon on plate&quot); return new Bacon(); } private static Egg FryEggs(int howMany) { Console.WriteLine(&quotWarming the egg pan...&quot); Task.Delay(3000).Wait(); Console.WriteLine($&quotcracking {howMany} eggs&quot); Console.WriteLine(&quotcooking the eggs ...&quot); Task.Delay(3000).Wait(); Console.WriteLine(&quotPut eggs on plate&quot); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine(&quotPouring coffee&quot); return new Coffee(); } } }
اماده کردن این صبحانه 30 دقیقه زمان میبرد.
اماده کردن این صبحانه 30 دقیقه زمان میبرد.


رایانه ها مثل انسان ها عمل نمیکنند. آنها قبل از اینکه سراغ عملیات بعدی بروند هر گذاره را مسدود میکنند تا کار کامل شود. به این معنی که تا کاری تمام نشود سراغ کار دیگری نمیروند. پس تهیه صبحانه بسیار زمان میبرد و برخی از اقلام هم قبل از سرو سرد میشوند.

اگر میخواهید رایانه این را به صورت ناهمزمان انجام دهد پس باید یک برنامه ناهمزمان بنویسید. این را دقت داشته باشید که وقتی برای یک کلاینت برنامه مینویسید UI شما باید پاسخگوی ورودی های کاربر باشد، به این معنی که شما نمیتوانید برای وارد کردن یک شماره تلفن، کاربر را منتظر نگه دارید تا عکس پرسنلی آن به سیستم آپلود شود.

مسدود نکنید، منتظر بمانید!

در کد قبل یک اتفاق بد رخ میدهد: نوشتن یک کد همزمان برای یک عملیات ناهمزمان. همان طور که نوشته شده، این کد از همان ابتدای اجرا thread را مسدود میکند. تا زمانی که یک عملیات به اتمام نرسد عملیات دیگری آغاز نخواهد شد.

این کد را به نحوی تغییر میدهیم که عملیات مربوطه در زمانی که اجرا میشود thread را مسدود نکند. استفاده از await یک راه بدون مسدودیت برای اجرای یک task است، و بعد از اتمام آن task اجرا را ادامه دهید.

یک نمونه ی ساده ی کد ناهمزمان برای آماده کردن صبحانه به صورت زیر است:

static async Task Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine(&quotcoffee is ready&quot); Egg eggs = await FryEggsAsync(2); Console.WriteLine(&quoteggs are ready&quot); Bacon bacon = await FryBaconAsync(3); Console.WriteLine(&quotbacon is ready&quot); Toast toast = await ToastBreadAsync(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine(&quottoast is ready&quot); Juice oj = PourOJ(); Console.WriteLine(&quotoj is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot); }
کل زمان سپری شده تقریباً همان نسخه اولیه همزمان است. این کد هنوز از برخی از ویژگی های اصلی برنامه نویسی ناهمزمان بهره نمی برد.

بدنه ی متدهای FryEggsAsync ، FryBaconAsync و ToastBreadAsync همه به روز شده اند تا Task<Bacon>، Task<Egg> و Task<Toast> را برگردانند. نام این متدها از نسخه اصلی خود تغییر یافته تا شامل پسوند "Async" شود. پیاده سازی آنها بعنوان بخشی از نسخه نهایی بعداً در این مقاله نشان داده شده است.

این کد در حین پختن تخم مرغ یا بیکن مسدود نمی شود. اگرچه این کد هیچ کار دیگری را شروع نمی کند. هنوز هم نان تست را در توستر می اندازید و به آن خیره می شوید تا زمانی که پخته شود. اما حداقل ، شما به هر کسی که توجه شما را بخواهد پاسخ می دهید. در رستورانی که چندین سفارش در آن انجام می شود ، آشپز می تواند صبحانه دیگری را هنگام اولین پخت و پز شروع کند.

برای برخی از برنامه ها این تغییر تمام چیزیست که لازم دارند. یک برنامه ی GUI (با رابط کاربری گرافیکی) همچنان فقط با همین تغییر به کاربر پاسخ میدهد. اما همچنان برای این سناریو شما بیشتر از این میخواهید. شما نمیخواهید که همه ی کارها به ترتیب انجام شوند. بهترین حالت این است که هر کار، قبل از اتمام کار قبلی شروع شود.

کارها را همزمان شروع کنید

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

System.Threading.Tasks.Task و انواع مرتبط با آن، کلاس هایی هستند که برای اجرای ناهمزمان به شما کمک میکنند و این امکان را به شما میدهند که کدی بیشتر شبیه به واقعیت بنویسید.

شما یک کار را شروع میکنید و شیء Taskرا که نشان دهنده ی ان کار است نگه میدارید.

اجازه دهید این تغییرات را در کد صبحانه ایجاد کنیم. اولین قدم ذخیره کارها برای شروع کارها به جای انتظار برای آنها است:

Coffee cup = PourCoffee(); Console.WriteLine(&quotcoffee is ready&quot); Task<Egg> eggsTask = FryEggsAsync(2); Egg eggs = await eggsTask; Console.WriteLine(&quoteggs are ready&quot); Task<Bacon> baconTask = FryBaconAsync(3); Bacon bacon = await baconTask; Console.WriteLine(&quotbacon is ready&quot); Task<Toast> toastTask = ToastBreadAsync(2); Toast toast = await toastTask; ApplyButter(toast); ApplyJam(toast); Console.WriteLine(&quottoast is ready&quot); Juice oj = PourOJ(); Console.WriteLine(&quotoj is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot);

بعد ، می توانید پیش از سرو صبحانه ، عبارات انتظار برای بیکن و تخم مرغ را به انتهای روش منتقل کنید:

Coffee cup = PourCoffee(); Console.WriteLine(&quotcoffee is ready&quot); Task<Egg> eggsTask = FryEggsAsync(2); Task<Bacon> baconTask = FryBaconAsync(3); Task<Toast> toastTask = ToastBreadAsync(2); Toast toast = await toastTask; ApplyButter(toast); ApplyJam(toast); Console.WriteLine(&quottoast is ready&quot); Juice oj = PourOJ(); Console.WriteLine(&quotoj is ready&quot); Egg eggs = await eggsTask; Console.WriteLine(&quoteggs are ready&quot); Bacon bacon = await baconTask; Console.WriteLine(&quotbacon is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot);

آماده شدن این صبحانه به صورت ناهمزمان 20 دقیقه زمان میبرد.

کد آخر بهتر کار میکند. همه ی کارهای ناهمزمان به یکباره شروع میشود. شما فقط در صورت نیاز به نتیجه در انتظار کار باقی میمانید (مانند آماده شدن تخم مرغ و نان تست).کد قبلی ممکن است مشابه کد موجود در یک برنامه وب باشد که درخواست را به میکروسرویس های مختلف می دهد ، سپس نتایج را در یک صفحه واحد ترکیب می کند. شما بلافاصله تمام درخواست ها را ارسال میکنید و منتظر نتیجه میمانید و صفحه وب را نمایش میدهید.

ترکیبِ وظایف

همه چیز را به طور همزمان برای صبحانه آماده دارید به جز نان تست. تهیه نان تست ترکیب یک عملیات ناهمزمان (تست نان) و عملیات همزمان (افزودن کره و مربا) است. به روزرسانی این کد مفهوم مهمی را نشان می دهد:

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

کد قبل به شما نشان داد که میتوانید از اشیاء Task یا Task<TResult> برای نگه داشتن کارهای در حال انجام استفاده کنید. برای استفاده از نتیجه ی هر تسک باید منتظر اتمام آن باشید(استفاده از await). مرحله ی بعد ترکیب کارها با یکدیگر است. قبل از سرو صبحانه، میخواهید قبل از افزودن کره و مربا، منتظر اتمام کار مربوط به تست شدن نان باشید. این کار را با کد زیر میتوانید انجام دهید:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number) { var toast = await ToastBreadAsync(number); ApplyButter(toast); ApplyJam(toast); return toast; }

در این تابع از async استفاده شده است. این عبارت به compiler نشان میدهد که این قسمت حاوی یک await است و شامل یک کد ناهمزمان است. این کد نان تست را آماده میکند و بعد کره و مربا را به آن اضافه میکند. مقدار بازگشتی این تابع از نوع Task<TResult> است که ترکیب این سه عملیات را نشان میدهد. تابع Main این کد به صورت زیر تغییر پیدا میکند:

static async Task Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine(&quotcoffee is ready&quot); var eggsTask = FryEggsAsync(2); var baconTask = FryBaconAsync(3); var toastTask = MakeToastWithButterAndJamAsync(2); var eggs = await eggsTask; Console.WriteLine(&quoteggs are ready&quot); var bacon = await baconTask; Console.WriteLine(&quotbacon is ready&quot); var toast = await toastTask; Console.WriteLine(&quottoast is ready&quot); Juice oj = PourOJ(); Console.WriteLine(&quotoj is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot); }

تغییر قبلی تکییک مهمی را برای کار با کد ناهمزمان نشان می دهد. شما با جدا کردن عملیات به یک متد جدید که یک کار را برمی گرداند ، وظایف را می سازید. می توانید انتخاب کنید که چه زمانی منتظر آن کار باشید. می توانید همزمان کارهای دیگر را نیز شروع کنید.

انتظارِ بهینه و مؤثرتر

کدهای قبلی را با اضافه کردن چند متد که با کلاس های مربوط به Task پیاده سازی شده باشند میتوان بهینه تر کرد. یکی از آنها WhenAll است که نتیجه ی آن یک Task است که اتمام آن به معنی تمام شدن همه ی کارهایی است که به این تابع سپرده شده اند، همان طور که در کد زیر میبینید:

await Task.WhenAll(eggsTask, baconTask, toastTask); Console.WriteLine(&quoteggs are ready&quot); Console.WriteLine(&quotbacon is ready&quot); Console.WriteLine(&quottoast is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot);

گذینه ی بعد استفاده از WhenAny است، که یک مقدار Task<Task> است که هر زمان یکی از کارهایی که به آن سپرده شده است تمام شود، بازگردانده میشود. با دانستن اینکه این کار قبلاً به پایان رسیده است ، می توانید منتظر کار برگشتی باشید. کد زیر نشان می دهد که چگونه می توانید از WhenAny برای انتظار اولین کار برای پایان کار و سپس پردازش نتیجه آن استفاده کنید. پس از پردازش نتیجه ی کار انجام شده، شما آن کار کامل شده را از لیست وظایف منتقل شده به WhenAny حذف می کنید.

var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask }; while (breakfastTasks.Count > 0) { Task finishedTask = await Task.WhenAny(breakfastTasks); if (finishedTask == eggsTask) { Console.WriteLine(&quoteggs are ready&quot); } else if (finishedTask == baconTask) { Console.WriteLine(&quotbacon is ready&quot); } else if (finishedTask == toastTask) { Console.WriteLine(&quottoast is ready&quot); } breakfastTasks.Remove(finishedTask); }

بعد از همه ی این تغییرات، نسخه ی نهایی کد به صورت زیر میشود:

using System; using System.Collections.Generic; using System.Threading.Tasks; namespace AsyncBreakfast { class Program { static async Task Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine(&quotcoffee is ready&quot); var eggsTask = FryEggsAsync(2); var baconTask = FryBaconAsync(3); var toastTask = MakeToastWithButterAndJamAsync(2); var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask }; while (breakfastTasks.Count > 0) { Task finishedTask = await Task.WhenAny(breakfastTasks); if (finishedTask == eggsTask) { Console.WriteLine(&quoteggs are ready&quot); } else if (finishedTask == baconTask) { Console.WriteLine(&quotbacon is ready&quot); } else if (finishedTask == toastTask) { Console.WriteLine(&quottoast is ready&quot); } breakfastTasks.Remove(finishedTask); } Juice oj = PourOJ(); Console.WriteLine(&quotoj is ready&quot); Console.WriteLine(&quotBreakfast is ready!&quot); } static async Task<Toast> MakeToastWithButterAndJamAsync(int number) { var toast = await ToastBreadAsync(number); ApplyButter(toast); ApplyJam(toast); return toast; } private static Juice PourOJ() { Console.WriteLine(&quotPouring orange juice&quot); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine(&quotPutting jam on the toast&quot); private static void ApplyButter(Toast toast) => Console.WriteLine(&quotPutting butter on the toast&quot); private static async Task<Toast> ToastBreadAsync(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine(&quotPutting a slice of bread in the toaster&quot); } Console.WriteLine(&quotStart toasting...&quot); await Task.Delay(3000); Console.WriteLine(&quotRemove toast from toaster&quot); return new Toast(); } private static async Task<Bacon> FryBaconAsync(int slices) { Console.WriteLine($&quotputting {slices} slices of bacon in the pan&quot); Console.WriteLine(&quotcooking first side of bacon...&quot); await Task.Delay(3000); for (int slice = 0; slice < slices; slice++) { Console.WriteLine(&quotflipping a slice of bacon&quot); } Console.WriteLine(&quotcooking the second side of bacon...&quot); await Task.Delay(3000); Console.WriteLine(&quotPut bacon on plate&quot); return new Bacon(); } private static async Task<Egg> FryEggsAsync(int howMany) { Console.WriteLine(&quotWarming the egg pan...&quot); await Task.Delay(3000); Console.WriteLine($&quotcracking {howMany} eggs&quot); Console.WriteLine(&quotcooking the eggs ...&quot); await Task.Delay(3000); Console.WriteLine(&quotPut eggs on plate&quot); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine(&quotPouring coffee&quot); return new Coffee(); } } }

نسخه نهایی صبحانه غیرهمزمان تقریباً 15 دقیقه به طول انجامید ، زیرا برخی از وظایف به طور همزمان قابل اجرا بودند و کد قادر بود چندین کار را همزمان کنترل کند و فقط در صورت لزوم اقدامات لازم را انجام دهد.


همزمانناهمزمانasyncawaitasynchronously
شاید از این پست‌ها خوشتان بیاید