مدل برنامه نویسی Async و Await

جمعه 3 اردیبهشت 1395

در این مقاله به معرفی کلمات کلیدی Async و Await خواهیم پرداخت. با استفاده از مکانیزم async می توانیم عملیات های طولانی در حال اجرا را متوقف کرده و وظایف دیگری را انجام دهیم.

مدل برنامه نویسی Async و Await

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

با استفاده از مکانیزم async می توانیم عملیات های طولانی در حال اجرا را متوقف کرده و وظایف دیگری را انجام دهیم. آن عملیات های طولانی کار خود را در thread های دیگر انجام می دهند و زمانی که به پایان رسیدند ، کد اصلی را مطلع میکنند و کد ما عملی را از اینجا پست کند. کد ما ، thread اصلی ای است که با رابط کاربری در ارتباط است یا thread ای است که در درجه اول فرایند یک web request  یا service UI را به عهده دارد. 

دو کلمه کلیدی جدید async و await در  NET 4.5. برای براحتی نوشتن برنامه های ناهمگام معرفی شدند که در سطح متد کار میکنند. البته نمی توانیم کلاسهایی را ایجاد کنیم که به صورت موازی کار کنند واجرای واحد داشته باشند.

threadها از همان ابتدا در چارچوب NET. وجود داشتند. Wrapperها بر روی سیستم عملیاتی بودندو کار با آنها سخت بود. بعضی مفاهیم مانند background worker, async delegate و Task Parallel Lirary برای آسان کردن مدل برنامه نویسی ناهمگام به عنوان بخشی از Class library آمده اند. برای درک بهتر async و await به مثال زیر توجه کنید که فاکتوریل  N عدد را پیدا میکند.

public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorial(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}
private void WriteFactorial(int no)
{
    int result = FindFactorialWithSimulatedDelay(no);
    Console.WriteLine("Factorial of {0} is {1}", no, result);
}
private static int FindFactorialWithSimulatedDelay(int no)
{
    int result = 1;
    for (int i = 1; i <= no; i++)
    {
         Thread.Sleep(500);
         result = result * i;
    }
    return result;
}

می توانیم یک loop راببینیم که از 1 تا 5 با استفاده از متغیر Counter اجرا می شود. مقدار شمارنده جاری را که کاملا بر 3 بخشپذیر باشد پیدا میکند ، و اگر چنین بود فاکتوریل آن را می نویسد. تابع نوشته شده فاکتوریل را بوسیله فراخوانی ()FindFactorialWithSimulatedDelay محاسبه میکند. این متد در اینجا برای ایجاد تاخیر برای شبیه سازی حجم کار زندگی واقعی تعبیه شده است. در حالات دیگر این یک عملیات اجرایی طولانی است.

براحتی می توانیم ببینیم که اجرا به طور متوالی اتفاق میافتد. فراخوان  ()WriteFactorial در حلقه منتظر میماند تا فاکتویل محاسبه شود. چرا باید صبر کنیم؟ چرا نمی توانیم به عدد بعدی برویم؟  دستور Console.Writeline  در ()WriteFactorial  باید تا زمانی که فاکتوریل یافت میشود  در انتظار باشد. به این معنا که می توانیم ()FindFactorialWithSimulatedDelay را به طور نا همزمان فراخوانی کنیم که یک call back  را به WriteFactorial فراهم کرده است. هنگامی که یک درخواست async اتفاق میافتد، شمارنده حلقه به عدد بعدی می رود و ()WriteFactorial را فراخوانی میکند.

Threading  یک راه برای حل این مشکل است. اما از آنجا که سخت است و به دانش بیشتری نیاز دارد ، از مکانیزم async() delegates استفاده میکنیم. در زیر متد WriteFactoeial را با استفاده از() async delegate بازنویسی کردیم.

private void WriteFactorialAsyncUsingDelegate(int facno)
{
    Func<int, int> findFact = FindFactorialWithSimulatedDelay;
    findFact.BeginInvoke(facno, 
                        (iAsyncresult) =>
                                {
                                    AsyncResult asyncResult = iAsyncresult as AsyncResult;
                                    Func<int, int> del = asyncResult.AsyncDelegate as Func<int, int>;
                                    int factorial = del.EndInvoke(asyncResult);
                                    Console.WriteLine("Factorial of {0} is {1}", facno, factorial);
                                }, 
                        null);
}

public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingDelegate(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

یکی از ساده ترین متدهایی که استفاده میشد ، استفاده از Asynchronous Delegate Ivocation بود. از مکانیزم فراخوانی متد Begin/End استفاده میکند. در اینجا زمان-اجرا از یک  thread  برای اجرا کردن کد استفاده میکند و بعد از تکمیل آن می توانیم call back داشته باشیم.

private void WriteFactorialAsyncUsingDelegate(int facno)
{
    Func<int, int> findFact = FindFactorialWithSimulatedDelay;
    findFact.BeginInvoke(facno, 
                        (iAsyncresult) =>
                                {
                                    AsyncResult asyncResult = iAsyncresult as AsyncResult;
                                    Func<int, int> del = asyncResult.AsyncDelegate as Func<int, int>;
                                    int factorial = del.EndInvoke(asyncResult);
                                    Console.WriteLine("Factorial of {0} is {1}", facno, factorial);
                                }, 
                        null);
}

public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingDelegate(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

براحتی تابع جدیدی با نام() WriteFactorialAsyncUsingDelegate را اضافه کردیم. و Main  را برای فراخوانی این متد در loop اصلاح کردیم.  به محض اینکه BeginInvoke بر روی findFact delegate فراخوانی شود، Thread  اصلی به شمارنده حلقه باز میگردد، و شمارنده افزایش یافته و loop  ادامه پیدا میکند.

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

بهبود برنامه نویسی async با استفاده از TPL

TPL در NET. 4.0. معرفی شد. کدهای async را می توانیم در یک شیء Task پنهان کرده و آن را اجرا کنیم. می توانیم برای تکمیل شدن یک یا چند وظیفه در انتظار باشیم. براحتی می توانیم وظیفه را متوقف کنیم. در زیر کد فاکتوریل را با استفاده از TPL بازنویسی کردیم.

private void WriteFactorialAsyncUsingTask(int no)
{
    Task<int> task=Task.Run<int>(() =>
    {
        int result = FindFactorialWithSimulatedDelay(no);
        return result;
    });
    task.ContinueWith(new Action<Task<int>>((input) =>
    {
        Console.WriteLine("Factorial of {0} is {1}", no, input.Result);
    }));
}
public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingTask(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

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

هنوز هم این یکی از ویژگیهای زبان نیست و لازم است برای دریافت پشتیبانی به کتابخانه های TPL مراجعه کنیم. مشکل اصلی در اینجا تلاش برای نوشتن کنترل رویداد تکمیل شده است . در ادامه خواهیم دید چگونه می توان نمونه TPL را با استفاده از کلمات کلیدی async و await بازنویسی کرد.

در متد WriteFactoialAsyncUsingAwait از کلمه کلیدی async استفاده میکنیم که بر انجام عملیات تابع به شیوه async دلالت دارد. ممکن است شامل کلمه await هم باشد . بدون async نمی توانیم await  داشته باشیم.

سپس بر روی تابع پیدا کردن فاکتوریل منتظر میمانیم. لحظه ای که در هنگام اجرا با await مواجه شویم ، thread  متد را فراخوانی میکند و بواسطه آن نتایج اجرا می شوند. کدی که در انتظار بوده با استفاده از TPL به عنوان وظیفه اش اجرا خواهد شد. به صورت نرمال یک thread  را از pool  گرفته و آن را اجرا میکند. هنگامی که اجرا کامل شد، await شرح داده شده اجرا خواهد شد.

در FindFactorialWithSimulatedDelay هیچ چیز راتغییر نداده ایم.

private async Task WriteFactorialAsyncUsingAwait(int facno)
{
    int result = await Task.Run(()=> FindFactorialWithSimulatedDelay(facno));
    Console.WriteLine("Factorial of {0} is {1}", facno, result);
}
public void Main()
{
    for (int counter = 1; counter < 5; counter++)
    {
        if (counter % 3 == 0)
        {
            WriteFactorialAsyncUsingAwait(counter);
        }
        else
        {
            Console.WriteLine(counter);
        }
    }
    Console.ReadLine();
}

کلمات کلیدی async و await استفاده داخلی از TPL را فراهم میکنند. به طور واضح تر می توان گفت async و await ترکیبی شیرین در زبان #C هستند. به عبارت دیگر زمان اجرای Net. چیزی درباره این کلمات نمی داند.

از async  و await می توانیم در هر جایی که برای چیزی منتظر می مانیم استفاده کنیم. برای مثال فایلهای IO ، عملیات های شبکه ، عملیات های دیتابیس و ... که به واکنش گرا بودن UI کمک میکند.

آموزش سی شارپ

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

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

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

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