برنامه نویسی Synchronous
که برنامه نویسی همزمان، همگام هم گفته میشود. در این برنامه نویسی دستورات به محض فراخوانی اجرا میشوند و بعد از اتمام یک عملیات، عملیات بعدی شروع خواهد شد.
برنامه نویسی Asynchronous
برنامه نویسی ناهمزمان یا ناهمگام هم گفته میشود. در این برنامه نویسی عملیات های فراخوانی شده در لحظه فراخوانی شروع به اجرا و اتمام آنها با توجه به پردازشی که انجام میدهند متغیر است. و ممکن است ترتیب اتمام با ترتیب شروع اجرا یکسان نباشد.
برای توضیح بهتر این قسمت آماده شدن یک صبحانه رو تصور کنید که شامل قسمت های زیر باشد:
اگر تجربه ی آشپزی داشته باشید، شما این دستور العمل را به صورت ناهمزمان انجام میدهید. شما میتوانید قبل از اینکه یک کار رو به اتمام برسونید کار دیگری را شروع کنید.
کد این قسمت رو به صورت همزمان ببینیم:
using System; using System.Threading.Tasks; namespace AsyncBreakfast { class Program { static void Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Egg eggs = FryEggs(2); Console.WriteLine("eggs are ready"); Bacon bacon = FryBacon(3); Console.WriteLine("bacon is ready"); Toast toast = ToastBread(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); } private static Juice PourOJ() { Console.WriteLine("Pouring orange juice"); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine("Putting jam on the toast"); private static void ApplyButter(Toast toast) => Console.WriteLine("Putting butter on the toast"); private static Toast ToastBread(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("Putting a slice of bread in the toaster"); } Console.WriteLine("Start toasting..."); Task.Delay(3000).Wait(); Console.WriteLine("Remove toast from toaster"); return new Toast(); } private static Bacon FryBacon(int slices) { Console.WriteLine($"putting {slices} slices of bacon in the pan"); Console.WriteLine("cooking first side of bacon..."); Task.Delay(3000).Wait(); for (int slice = 0; slice < slices; slice++) { Console.WriteLine("flipping a slice of bacon"); } Console.WriteLine("cooking the second side of bacon..."); Task.Delay(3000).Wait(); Console.WriteLine("Put bacon on plate"); return new Bacon(); } private static Egg FryEggs(int howMany) { Console.WriteLine("Warming the egg pan..."); Task.Delay(3000).Wait(); Console.WriteLine($"cracking {howMany} eggs"); Console.WriteLine("cooking the eggs ..."); Task.Delay(3000).Wait(); Console.WriteLine("Put eggs on plate"); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine("Pouring coffee"); return new Coffee(); } } }
رایانه ها مثل انسان ها عمل نمیکنند. آنها قبل از اینکه سراغ عملیات بعدی بروند هر گذاره را مسدود میکنند تا کار کامل شود. به این معنی که تا کاری تمام نشود سراغ کار دیگری نمیروند. پس تهیه صبحانه بسیار زمان میبرد و برخی از اقلام هم قبل از سرو سرد میشوند.
اگر میخواهید رایانه این را به صورت ناهمزمان انجام دهد پس باید یک برنامه ناهمزمان بنویسید. این را دقت داشته باشید که وقتی برای یک کلاینت برنامه مینویسید UI شما باید پاسخگوی ورودی های کاربر باشد، به این معنی که شما نمیتوانید برای وارد کردن یک شماره تلفن، کاربر را منتظر نگه دارید تا عکس پرسنلی آن به سیستم آپلود شود.
در کد قبل یک اتفاق بد رخ میدهد: نوشتن یک کد همزمان برای یک عملیات ناهمزمان. همان طور که نوشته شده، این کد از همان ابتدای اجرا thread را مسدود میکند. تا زمانی که یک عملیات به اتمام نرسد عملیات دیگری آغاز نخواهد شد.
این کد را به نحوی تغییر میدهیم که عملیات مربوطه در زمانی که اجرا میشود thread را مسدود نکند. استفاده از await یک راه بدون مسدودیت برای اجرای یک task است، و بعد از اتمام آن task اجرا را ادامه دهید.
یک نمونه ی ساده ی کد ناهمزمان برای آماده کردن صبحانه به صورت زیر است:
static async Task Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Egg eggs = await FryEggsAsync(2); Console.WriteLine("eggs are ready"); Bacon bacon = await FryBaconAsync(3); Console.WriteLine("bacon is ready"); Toast toast = await ToastBreadAsync(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); }
کل زمان سپری شده تقریباً همان نسخه اولیه همزمان است. این کد هنوز از برخی از ویژگی های اصلی برنامه نویسی ناهمزمان بهره نمی برد.
بدنه ی متدهای FryEggsAsync ، FryBaconAsync و ToastBreadAsync همه به روز شده اند تا Task<Bacon>، Task<Egg> و Task<Toast> را برگردانند. نام این متدها از نسخه اصلی خود تغییر یافته تا شامل پسوند "Async" شود. پیاده سازی آنها بعنوان بخشی از نسخه نهایی بعداً در این مقاله نشان داده شده است.
این کد در حین پختن تخم مرغ یا بیکن مسدود نمی شود. اگرچه این کد هیچ کار دیگری را شروع نمی کند. هنوز هم نان تست را در توستر می اندازید و به آن خیره می شوید تا زمانی که پخته شود. اما حداقل ، شما به هر کسی که توجه شما را بخواهد پاسخ می دهید. در رستورانی که چندین سفارش در آن انجام می شود ، آشپز می تواند صبحانه دیگری را هنگام اولین پخت و پز شروع کند.
برای برخی از برنامه ها این تغییر تمام چیزیست که لازم دارند. یک برنامه ی GUI (با رابط کاربری گرافیکی) همچنان فقط با همین تغییر به کاربر پاسخ میدهد. اما همچنان برای این سناریو شما بیشتر از این میخواهید. شما نمیخواهید که همه ی کارها به ترتیب انجام شوند. بهترین حالت این است که هر کار، قبل از اتمام کار قبلی شروع شود.
در بسیاری از سناریو ها، میخواهید بلافاصله چندین کار مستقل را شروع کنید. سپس با اتمام هر کار، میتوانید کارهای اماده ی دیگر را ادامه دهید. در سناریوی صبحانه، با این روند، صبحانه با سرعت بیشتری آماده میشود.همچنین همه ی کارها را تقریبا همزمان انجام میدهید و در نهایت یک صبحانه ی گرم دریافت خواهید کرد.
System.Threading.Tasks.Task و انواع مرتبط با آن، کلاس هایی هستند که برای اجرای ناهمزمان به شما کمک میکنند و این امکان را به شما میدهند که کدی بیشتر شبیه به واقعیت بنویسید.
شما یک کار را شروع میکنید و شیء Taskرا که نشان دهنده ی ان کار است نگه میدارید.
اجازه دهید این تغییرات را در کد صبحانه ایجاد کنیم. اولین قدم ذخیره کارها برای شروع کارها به جای انتظار برای آنها است:
Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Task<Egg> eggsTask = FryEggsAsync(2); Egg eggs = await eggsTask; Console.WriteLine("eggs are ready"); Task<Bacon> baconTask = FryBaconAsync(3); Bacon bacon = await baconTask; Console.WriteLine("bacon is ready"); Task<Toast> toastTask = ToastBreadAsync(2); Toast toast = await toastTask; ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!");
بعد ، می توانید پیش از سرو صبحانه ، عبارات انتظار برای بیکن و تخم مرغ را به انتهای روش منتقل کنید:
Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); 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("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Egg eggs = await eggsTask; Console.WriteLine("eggs are ready"); Bacon bacon = await baconTask; Console.WriteLine("bacon is ready"); Console.WriteLine("Breakfast is ready!");
آماده شدن این صبحانه به صورت ناهمزمان 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("coffee is ready"); var eggsTask = FryEggsAsync(2); var baconTask = FryBaconAsync(3); var toastTask = MakeToastWithButterAndJamAsync(2); var eggs = await eggsTask; Console.WriteLine("eggs are ready"); var bacon = await baconTask; Console.WriteLine("bacon is ready"); var toast = await toastTask; Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); }
تغییر قبلی تکییک مهمی را برای کار با کد ناهمزمان نشان می دهد. شما با جدا کردن عملیات به یک متد جدید که یک کار را برمی گرداند ، وظایف را می سازید. می توانید انتخاب کنید که چه زمانی منتظر آن کار باشید. می توانید همزمان کارهای دیگر را نیز شروع کنید.
کدهای قبلی را با اضافه کردن چند متد که با کلاس های مربوط به Task پیاده سازی شده باشند میتوان بهینه تر کرد. یکی از آنها WhenAll است که نتیجه ی آن یک Task است که اتمام آن به معنی تمام شدن همه ی کارهایی است که به این تابع سپرده شده اند، همان طور که در کد زیر میبینید:
await Task.WhenAll(eggsTask, baconTask, toastTask); Console.WriteLine("eggs are ready"); Console.WriteLine("bacon is ready"); Console.WriteLine("toast is ready"); Console.WriteLine("Breakfast is ready!");
گذینه ی بعد استفاده از 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("eggs are ready"); } else if (finishedTask == baconTask) { Console.WriteLine("bacon is ready"); } else if (finishedTask == toastTask) { Console.WriteLine("toast is ready"); } 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("coffee is ready"); 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("eggs are ready"); } else if (finishedTask == baconTask) { Console.WriteLine("bacon is ready"); } else if (finishedTask == toastTask) { Console.WriteLine("toast is ready"); } breakfastTasks.Remove(finishedTask); } Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); } static async Task<Toast> MakeToastWithButterAndJamAsync(int number) { var toast = await ToastBreadAsync(number); ApplyButter(toast); ApplyJam(toast); return toast; } private static Juice PourOJ() { Console.WriteLine("Pouring orange juice"); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine("Putting jam on the toast"); private static void ApplyButter(Toast toast) => Console.WriteLine("Putting butter on the toast"); private static async Task<Toast> ToastBreadAsync(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("Putting a slice of bread in the toaster"); } Console.WriteLine("Start toasting..."); await Task.Delay(3000); Console.WriteLine("Remove toast from toaster"); return new Toast(); } private static async Task<Bacon> FryBaconAsync(int slices) { Console.WriteLine($"putting {slices} slices of bacon in the pan"); Console.WriteLine("cooking first side of bacon..."); await Task.Delay(3000); for (int slice = 0; slice < slices; slice++) { Console.WriteLine("flipping a slice of bacon"); } Console.WriteLine("cooking the second side of bacon..."); await Task.Delay(3000); Console.WriteLine("Put bacon on plate"); return new Bacon(); } private static async Task<Egg> FryEggsAsync(int howMany) { Console.WriteLine("Warming the egg pan..."); await Task.Delay(3000); Console.WriteLine($"cracking {howMany} eggs"); Console.WriteLine("cooking the eggs ..."); await Task.Delay(3000); Console.WriteLine("Put eggs on plate"); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine("Pouring coffee"); return new Coffee(); } } }
نسخه نهایی صبحانه غیرهمزمان تقریباً 15 دقیقه به طول انجامید ، زیرا برخی از وظایف به طور همزمان قابل اجرا بودند و کد قادر بود چندین کار را همزمان کنترل کند و فقط در صورت لزوم اقدامات لازم را انجام دهد.