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

عملگر های async-await یک روند(process) دیگر را به لیست اضافه می کنند و در برخی شرایط ممکن است باعث مشکلاتی شود.

قبل از شروع صحبت در مورد موقعیت های دشوار، به طور خلاصه چند مفهوم اساسی را توضیح خواهم داد.

روند(process)

هنگامی که یک برنامه کامپیوتری شروع می شود، سیستم عامل(OS) یک روند برای اجرای برنامه ایجاد می کند، معمولاً، شما می توانید شناسه روند را در مدیر برنامه در سیستم عامل خود مشاهده کنید، هنگام بستن این روند برنامه شما به پایان می رسد.

رشته(thread)

thread زیرمجموعه ای از یک فرآیند است، از آن برای اجرای دستورالعمل های کد برنامه استفاده می شود، معمولاً یک فرآیند با یک رشته شروع می شود اما فرآیند می تواند رشته های اضافی را ایجاد و به پایان برساند.

هنگام بستن یک فرآیند، تمام رشته های مرتبط به پایان می رسند.

مسدود کردن یک رشته

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

هنگام شروع برنامه شما، یک رشته جدید ایجاد می شود و شروع به اجرای تمام دستورالعمل های کد، مانند نمایش رابط کاربری، ارائه محتوا، و بارگیری اجزای بصری مانند دکمه ها و جعبه های متنی می کند.

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

همین امر می تواند در برنامه هایی اتفاق بیفتد که رابط کاربری مانند API ندارند، در حالی که رشته یک کار را اجرا می کند، تمام دستورالعمل های دیگر باید منتظر بمانند.

چند رشته ای

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

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

ناهمگام(async)

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

اینجاست که async به مهمانی می رسد.

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

خیلی هوشمندانه است، اینطور نیست؟

تفاوت

در زیر می‌توانیم تفاوت‌های اجرایی بین sync، async و multi-thread را به صورت گرافیکی ببینیم.

حالت اجرای همگام

حالت غیر همگام

عملگر await

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

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

موقعیت های دشوار

استفاده از async-await تجربه توسعه‌دهنده را بسیار به رویکرد همگام‌سازی نزدیک می‌کند، اما در مواقعی هم ، اینطور نیست.

توسعه‌دهنده‌ای که دقیقاً نمی‌داند این عملگرها چگونه کار می‌کنند، همیشه از async-await استفاده می‌کند بدون اینکه به آنچه واقعاً اتفاق می‌افتد فکر کند، و هنگامی که چندین کار را انجام می‌دهیم، می‌توانیم برنامه‌ای را که به طور موازی آنها را اجرا می‌کند بهینه کنیم.

برای درک بهتر آن، مشکل زیر را ببینید

مشکل

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

من دو رویکرد را نشان خواهم داد و زمان صرف شده در اجرای هر روش را اندازه‌گیری می‌کنم.

public class AwaitingTest
{
    public async Task AwaitSequencially() { ... }

    public async Task AwaitInParallel() { ... }

    private async Task wait5sec() => await Task.Delay(5000);
    private async Task wait5sec_2() => await Task.Delay(5000);
    private async Task wait5sec_3() => await Task.Delay(5000);
}

روش غیر بهینه

در اینجا، من به طور ناهمزمان همه متدها را فراخوانی می کنم و به صورت متوالی منتظر آنها هستم.

public async Task AwaitSequencially()
{
    var watch = new Stopwatch();
    
    Console.WriteLine("Starting sequencially async calls");
    
    watch.Start();

    await wait5sec();
    await wait5sec_2();
    await wait5sec_3();

    watch.Stop();
    
    Console.WriteLine($"finished sequencially async calls in {watch.ElapsedMilliseconds}");
}

روش بهینه

اکنون، ابتدا همه کارها را ایجاد می کنم و سپس منتظر همه آنها هستم.

public async Task AwaitInParallel()
{
    var watch = new Stopwatch();
    
    Console.WriteLine("Starting parallel async calls");

    watch.Start();

    var a = wait5sec();
    var b = wait5sec_2();
    var c = wait5sec_3();

    Console.WriteLine("Doing other stuff");

    await a;
    await b;
    await c;
    
    watch.Stop();
    
    Console.WriteLine($"finished parallel async calls in {watch.ElapsedMilliseconds}");
}

خروجی

فراخوانی این دو روش نتایج بسیار متفاوتی خواهد داشت

class Program
{
    static void Main(string[] args)
    {
        var test = new AwaitingTest();

        Console.WriteLine("Starting app");

        test.AwaitSequencially().Wait();
        test.AwaitInParallel().Wait();

        Console.WriteLine("Ending app");
    }
}

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

روش اول بعد از 15 ثانیه تمام شد، در حالی که روش دوم فقط 5 ثانیه طول کشید زیرا همه تماس ها را به صورت موازی اجرا می کرد.

با اینکه مدل async-wait بسیار شبیه به مدل همگام‌سازی است، آنها یکسان نیستند و عدم درک این تفاوتها میتواند مشکل ساز باشد.

امیدوارم این مقاله بتواند به شما و تیمتان کمک کند تا آن را بهتر درک کنید،  از خواندن آن متشکرم.

 

منبع : betterprogramming.pub