پیاده سازی Async Await

یکشنبه 6 مهر 1399

هدف از نوشتن این مطلب، شناسایی مشکلات و اشتباهات رایجی است که هنگام پیاده سازی رخ می دهد، زیرا Async / Await یک بحث پیچیده است و حتی یک اشتباه کوچک یا پیاده سازی اشتباه منجر به عدم ثبات سیستم ها می شود. ایده این نیست که چرخ را دوباره اختراع کنیم، بلکه تصاحب بهترین شیوه ها و دستورالعمل های به اشتراک گذاشته شده ‌ی جوامع بزرگتر است.

پیاده سازی Async Await

علاوه بر این برای ما مهم است که اصول را کاملاً رعایت کنیم و با احتیاط از این دستورالعمل ها پیروی کنیم. همه ما به عنوان یک تیم باید اطمینان حاصل کنیم که از این موارد به درستی استفاده می کنیم.

مباحث

● جلوگیری ازاستفاده کورکورانه از Async/Await درهرجا

● راهنمای عملکرد پسوند Async به وسیله Async

● استفاده از ()Task.FromResult درصورت وجود مقدار بازگشتی و استفاده از  Task.CompletedTask() درصورت عدم وجود مقدار بازگشتی، درهنگام پیاده سازی Task  اصلی

● بررسی لغو توکن، یک پارامترتکمیلی برای لغو task/request مفید خواهد بود

● به جای استفاده از .Result ، ازGetAwaiter().GetResult استفاده کنید. این کار تأثیر زیادی در مدیریت خطاها و خطاهای تجمعی دارد. سعی کنید از این سناریو اجتناب کنید. به دلیل بحث Synchronization، در یک موقعیت قفل‌شده قرارمی گیرد.

● وقتی به sync context نیاز ندارید از ConfigureAwaiter(false) استفاده کنید.

● ازreturn  برای دستور using استفاده نکنید.

● ازEF نمی توان به طور موازی (parallel) استفاده کرد. await.Task.WhenAll یا WhenAny. هنگامی که ما از EF به وسیله یک پایگاه داده استفاده می کنیم ، امن نیستند. از parallel فقط در مواردی که دارای رشته ایمن هستند استفاده کنید.

مورد اول: متد Async در یک متد void فراخوانی می شود

استفاده نادرست

    public static async Task FooAsync() {  
        // some async code here...    
        await Task.Delay(10000);  
    }  
    public void ThisWillNotWaitForAsyncCodeToComplete() {  
        try {  
            Console.WriteLine("Before : " + DateTime.Now.ToString());  
            FooAsync();  
            Console.WriteLine("After : " + DateTime.Now.ToString());  
        } catch (Exception ex) {  
            //The below line will never be reached    
            Console.WriteLine(ex.Message);  
        }  
    }  

استفاده درست

    public static async Task FooAsync() {  
        await Task.Delay(10000);  
    }  
    public async Task ThisWillNotWaitForAsyncCodeToCompleteAsync() {  
        Console.WriteLine("Before : " + DateTime.Now.ToString());  
        await FooAsync();  
        Console.WriteLine("After : " + DateTime.Now.ToString());  
    }  

تغییراتی که باید با دقت به آن‌ها توجه شود:

FooAsync() -1با استفاده از await فراخوانی می شود.

 -2متد دارای async Task درsignature  است.

نتیجه

 -1حلقه Async / await به درستی دنبال شده

Parent thread -2 منتظر کامل شدن child thread خواهد بود درصورت انجام هرگونه دیتابیس یا عملیات رویداد، parent thread قبل ازترک کردن منتظر اتمام کار می ماند

 -3میزان دقت در این کار بالا است

مورد دوم: کلمه کلیدی "async" استفاده نشده و شی "task" بازگردانده نمی شود.

استفاده نادرست

    public static Task BarAsync() {  
        // some async code here...    
        return Task.Delay(10000);  
    }  
    public void ThisWillNotWaitForAsyncCodeToComplete() {  
        try {  
            Console.WriteLine("Before : " + DateTime.Now.ToString());  
            BarAsync();  
            Console.WriteLine("After : " + DateTime.Now.ToString());  
        } catch (Exception ex) {  
            //The below line will never be reached    
            Console.WriteLine(ex.Message);  
        }  
    }  

استفاده درست

    public static async Task BarAsync() {  
        await Task.Delay(10000);  
    }  
    public async Task ThisWillNotWaitForAsyncCodeToCompleteAsyncAsync() {  
        Console.WriteLine("Before : " + DateTime.Now.ToString());  
        await BarAsync();  
        Console.WriteLine("After : " + DateTime.Now.ToString());  
    }  

تغییراتی که باید با دقت توجه شود:

FooAsync() -1 از async استفاده می کند و task را برنمی گرداند.

2- ThisWillNotWaitForAsyncCodeToCompleteAsync() با استفاده از await، FooAsync() را فراخوانی می کند.

نتیجه

1- حلقه Async / await به درستی دنبال شده

2- Parent thread منتظر کامل شدن child thread خواهد بود. درصورت انجام هرگونه دیتابیس یا عملیات رویداد، parent thread قبل ازترک کردن منتظر اتمام کارمی ماند.

3- میزان دقت در این کار بالا است

مورد سوم: متد به عنوان "async Task" علامت گذاری شده است اما هیچ متد async ای در داخل فراخوانی نمی شود.

استفاده نادرست

    Public void Foo() {}  
    Public void Bar() {}  
    public async Task FakeAsyncMethod() {  
        Foo();  
        Bar();  
        Return Task.CompletedTask;  
    }  

استفاده درست

    Public void Foo() {}  
    Public void Bar() {}  
    public void FakeAsyncMethod() {  
        Foo();  
        Bar();  
    }  

تغییراتی که باید با دقت توجه شود:

1- حذف async task از ()FakeAsyncMethod

2- حذف دستور return task

نتیجه

1- به طور معمول متد sync را همانطور که انتظار می‌رود فراخوانی کنید.

2- میزان دقت در این کار بسیار مهم است.

مورد چهارم: async به جای استفاده ازawait، blocking call دارد

استفاده نادرست

    public static async Task FooAsync() {  
        await Task.Delay(10000);  
    }  
    public void ThisWillNotWaitForAsyncCodeToCompleteAsync() {  
        Console.WriteLine("Before : " + DateTime.Now.ToString());  
        FooAsync().Result;  
        Console.WriteLine("After : " + DateTime.Now.ToString());  
    }  

استفاده درست

public static async Task FooAsync() {  
    await Task.Delay(10000);  
}  
public void ThisWillNotWaitForAsyncCodeToCompleteAsync() {  
    Console.WriteLine("Before : " + DateTime.Now.ToString());  
    await FooAsync();  
    Console.WriteLine("After : " + DateTime.Now.ToString());  
}

تغییراتی که باید با دقت توجه شود:

1- اضافه کردن await در FooAync() برای دنبال کردن زنجیره مناسب async/await

2- استفاده از .Result مزیت async را از بین می برد.

نتیجه

1- متد Async را باید طبق توصیه و روش صحیح انجام آن، فراخوانی کنید.

2- میزان دقت این بخش مهم است

مورد پنجم: مسدود سازی متدasync  با استفاده از .Wait

استفاده نادرست

public async Task FooAsync(string id) {  
    …  
    some more  
    function code without any await operation inside…  
    await Task.Delay(10000);  
}  
public void Bar() {  
    console.writeline(“Hello world”);  
    FooAsync().Wait();  
}

استفاده درست

public async Task FooAsync(string id) {  
    …  
    some more  
    function code without any await operation inside…  
    await Task.Delay(10000);  
}  
public async Task BarAsync() {  
    console.writeline(“Hello world”);  
    await FooAsync();  
} 

تغییراتی که باید با دقت توجه شود:

1- از  async awaitدر BarAsync() استفاده کنید تا الگوی درست async/await را دنبال کنید.

نتیجه

1- متدasync  باید به روشی که باید انجام شود، فراخوانی شود به جای آنکه به زور به sync تبدیل شوند ورشته فعلی را مسدود کند.

2- میزان دقت این بخش مهم است

مورد ششم: ایجاد task برای متد sync و انتظار برای task

استفاده نادرست

    public void SomeMethod1() {  
        …  
        some  
        function code….  
        Var task = Task.Run(() => SomeMethod2);  
        task.Wait();…  
        some functional code….  
    }  
    public void SomeMethod2() {  
        …  
        Some  
        function code goes here...  
    }  

استفاده درست

    public void SomeMethod1() {  
        …  
        some  
        function code….  
        SomeMethod2();…  
        some functional code….  
    }  
    public void SomeMethod2() {  
        …  
        Some  
        function code goes here...  
    }  

تغییراتی که باید با دقت توجه شود:

به طور معمول از متد sync به روشی که مورد انتظار است استفاده کنید. ساختن یک task و انتظار برای task فقط هدر دادن یک thread اضافی در pool  است که همان کار را می توان در خود thread اصلی انجام داد.

نتیجه

1- عملکرد بهینه شده برای ایجاد task وقتی متدی باید منتظر بماند.

2- میزان دقت این بخش مهم است.

مورد هفتم: بازیافتن نتیجه چندین task

استفاده نادرست

    public async Task < string > FooAsync() {  
        string result = string.empty;…  
        some  
        function code…  
        return result;  
    }  
    public async Task < string > BarAsync() {  
        string result = string.empty;…  
        some  
        function code…  
        return result;  
    }  
    public void ParentMethod() {  
        var task1 = FooAsync();  
        var task2 = BarAsync();  
        Task.WaitAll(task1, task2);  
    } 

استفاده درست

    public async Task < string > FooAsync() {  
        string result = string.empty;…  
        some  
        function code…  
        return result;  
    }  
    public async Task < string > BarAsync() {  
        string result = string.empty;…  
        some  
        function code…  
        return result;  
    }  
    public async Task ParentMethod() {  
        var task1 = FooAsync();  
        var task2 = BarAsync();  
        await task.WhenAll(task1, task2);  
    } 

تغییراتی که باید با دقت انجام شود:

ما باید از میکس شدن blocking & unblocking کد جلوگیری کنیم. Task.WaitAll یک نام گذاری blocking است در حالی که Task.WhenAll،  nonblockin است و معنای async را حفظ می کند.

نتیجه

1- کد بهینه شده است و طبق دستورالعمل های برنامه نویسی async / await برای جلوگیری از مسدود شدن نامگذاری ها کار می کند.

2- میزان دقت این بخش بسیار مهم است

مورد هشتم: ورژن  Sync زمانی استفاده می شود که Async در دسترس باشد.

استفاده نادرست

    public bool CheckLabelAlreadyExist(string labelName, Guid facilityKey, int labelTypeCode) {  
        return GetQueryable().Any(x => x.DescriptionText == labelName && x.FacilityKey == facilityKey && x.LabelTypeCode == labelTypeCode);  
    }  

استفاده درست

public async Task < bool > CheckLabelAlreadyExist(string labelName, Guid facilityKey, int labelTypeCode) {  
    return await GetQueryable().AnyAsync(x => x.DescriptionText == labelName && x.FacilityKey == facilityKey && x.LabelTypeCode == labelTypeCode);  
} 

قوانین نامگذاری متد:

1- همه متدهای async باید دارای پسوند "async" در نام متد باشند تا خوانایی آسان شود و تمایز بین متدهای sync و async وجود داشته باشد.

2- داشتن "async" در متدها، آن را آشکار کرده و احتمال خطا در پیاده سازی را کاهش می دهد.

نتیجه گیری

async / wait بهترین کار برای task های محدود به IO (ارتباطات شبکه ای ، ارتباطات پایگاه داده ، درخواست http و غیره) است. اما خوب نیست که درtask های محاسباتی اعمال شود (پیمایش لیست بزرگ ، پردازش یک تصویر بزرگ و غیره). زیرا thread نگهدارنده را از  thread pool  آزاد می کند و CPU/coreهای دردسترس، برای پردازش آن در task ها به کارگرفته نمی شوند. بنابراین ، باید از استفاده Async / Await برای task های محاسباتی جلوگیری شود.
برای پرداختن به  taskهای محاسباتی، بهتر است ازTask.Factory.CreateNew  با TaskCreationOptions  که LongRunning است، استفاده کنیم. با استفاده از این روش تا زمان   انجام  task، یک background thread جدید برای پردازش یک  taskسنگین محاسباتی بدون آزاد کردن مجدد آن ازthread pool، شروع می شود.

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

نویسنده 3355 مقاله در برنامه نویسان
  • C#.net
  • 3k بازدید
  • 7 تشکر

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

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید