متدهای async تقلبی
تا اینجا مشاهده کردیم که اگر یک چنین متد زمانبری را داشته باشیم که در آن عملیاتی طولانی انجام می‌شود،
class MyService
{
  public int CalculateXYZ()
  {
    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;
    return 42;
  }
}
برای نوشتن معادل async آن فقط کافی است که امضای متد را به async Task تغییر دهیم و سپس داخل آن از Task.Run استفاده کنیم:
class MyService
{
  public async Task<int> CalculateXYZAsync()
  {
    return await Task.Run(() =>
    {
      // Tons of work to do in here!
      for (int i = 0; i != 10000000; ++i)
        ;
      return 42;
    });
  }
}
و ... اگر از آن در یک کد UI استفاده کنیم، ترد آن‌را قفل نکرده و برنامه، پاسخگوی سایر درخواست‌های رسیده خواهد بود. اما ... به این روش اصطلاحا Fake Async گفته می‌شود؛ یا Async تقلبی!
کاری که در اینجا انجام شده، استفاده‌ی ناصحیح از Task.Run در حین طراحی یک متد و یک API است. عملیات انجام شده در آن واقعا غیرهمزمان نیست و در زمان انجام آن، باز هم ترد جدید اختصاص داده شده را تا پایان عملیات قفل می‌کند. اینجا است که باید بین CPU-bound operations و IO-bound operations تفاوت قائل شد. اگر Entity Framework 6 و یا کلاس WebClient و امثال آن، متدهایی Async را نیز ارائه داده‌اند، این‌ها به معنای واقعی کلمه، غیرهمزمان هستند و در آن‌ها کوچکترین CPU-bound operation ایی انجام نمی‌شود.
در حلقه‌ای که در مثال فوق در حال پردازش است و یا تمام اعمال انجام شده توسط CPU، از مرزهای سیستم عبور نمی‌کنیم. نه قرار است فایلی را ذخیره کنیم، نه با اینترنت سر و کار داشته باشیم و یا مثلا اطلاعاتی را از وب سرویسی دریافت کنیم و نه هیچگونه IO-bound operation خاصی قرار است صورت گیرد.
زمانیکه برنامه نویسی قرار است با API شما کار کند و به امضای async Task می‌رسد، فرضش بر این است که در این متد واقعا یک کار غیرهمزمان در حال انجام است. بنابراین جهت بالابردن کارآیی برنامه، این نسخه را نسبت به نمونه‌ی غیرهمزمان انتخاب می‌کند.
حال تصور کنید که استفاده کننده از این API یک برنامه‌ی دسکتاپ نیست، بلکه یک برنامه‌ی ASP.NET است. در اینجا Task.Run فراخوانی شده صرفا سبب خواهد شد عملیات مدنظر، بر روی یک ترد دیگر، نسبت به ترد اصلی اختصاص داده شده توسط ASP.NET برای فراخوانی و پردازش CalculateXYZAsync، صورت گیرد. این عملیات بهینه نیست. تمام پردازش‌های درخواست‌های ASP.NET در تردهای خاص خود انجام می‌شوند. وجود ترد دوم ایجاد شده توسط Task.Run در اینجا چه حاصلی را بجز سوئیچ بی‌جهت بین تردها و همچنین بالا بردن میزان کار Garbage collector دارد؟ در این حالت نه تنها سبب بالا بردن مقیاس پذیری سیستم نشده‌ایم، بلکه میزان کار Garbage collector و همچنین سوئیچ بین تردهای مختلف را در Thread pool برنامه به شدت افزایش داده‌ایم. همچنین یک چنین سیستمی برای تدارک تردهای بیشتر و مدیریت آن‌ها، مصرف حافظه‌ی بیشتری نیز خواهد داشت.


یک اصل مهم در طراحی کدهای Async
استفاده از Task.Run در پیاده سازی بدنه متدهای غیرهمزمان، یک code smell محسوب می‌شود.


چکار باید کرد؟
اگر در کدهای خود اعمال Async واقعی دارید که IO-bound هستند، از معادل‌های Async طراحی شده برای کار با آن‌ها، مانند متد SaveChangesAsync در EF، متد DownloadStringTaskAsync کلاس WebClient و یا متدهای جدید Async کلاس Stream برای خواندن و نوشتن اطلاعات استفاده کنید. در یک چنین حالتی ارائه متدهای async Task بسیار مفید بوده و در جهت بالابردن مقیاس پذیری سیستم بسیار مؤثر واقع خواهند شد.
اما اگر کدهای شما صرفا قرار است بر روی CPU اجرا شوند و تنها محاسباتی هستند، اجازه دهید مصرف کننده تصمیم بگیرد که آیا لازم است از Task.Run برای فراخوانی متد ارائه شده در کدهای خود استفاده کند یا خیر. اگر برنامه‌ی دسکتاپ است، این فراخوانی مفید بوده و سبب آزاد شدن ترد UI می‌شود. اگر برنامه‌ی وب است، به هیچ عنوان نیازی به Task.Run نبوده و فراخوانی متداول آن با توجه به اینکه درخواست‌های برنامه‌های ASP.NET در تردهای مجزایی اجرا می‌شوند، کفایت می‌کند.

به صورت خلاصه
از Task.Run در پیاده سازی بدنه متدهای API خود استفاده نکنید.
از Task.Run در صورت نیاز (مثلا در برنامه‌های دسکتاپ) در حین فراخوانی و استفاده از متدهای API ارائه شده استفاده نمائید:
 private async void MyButton_Click(object sender, EventArgs e)
{
  await Task.Run(() => myService.CalculateXYZ());
}
در این مثال از همان نسخه‌ی غیرهمزمان متد محاسباتی استفاده شده‌است و اینبار مصرف کننده است که تصمیم گرفته در حین فراخوانی و استفاده نهایی، برای آزاد سازی ترد UI از await Task.Run استفاده کند (یا خیر).

بنابراین نوشتن یک چنین کدهایی در پیاده سازی یک API غیرهمزمان
await Task.Run(() =>
{
   for (int i = 0; i != 10000000; ++i)
     ;
});
صرفا خود را گول زدن است. کل این عملیات بر روی CPU انجام شده و هیچگاه از مرزهای IO سیستم عبور نمی‌کند.

برای مطالعه بیشتر
Should I expose asynchronous wrappers for synchronous methods
  • #
    ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۲۱:۲۸
    برای استفاده از یک وب سرویس، اگر به شکل زیر در یک ClassLibrary استفاده شود، آیا واقعا غیر همزمان عمل می‌شود یا تقلبی محسوب می‌شود؟
    public bool SendSms()
    {
        try
        {
            var result = false;
            foreach (var nomber in _fromNombers)
            {
                if (result) continue;
                if (_smsService.SendGroupSmsSimple(Model.Signature, nomber, Model.Numbers, Model.Body, false,
                    string.Empty) == 1)
                    result = true;
            }
            return result;
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception.Message);
            return false;
        }
    }
    
    private Task SendSmsAsync()
    {
        return Task.Run(() => SendSms());
    }
    و مثلا در یک ConsoleApp:
    static void Main(string[] args)
    {
        var r = SendAsync();
        r.Wait();
    }
    
    private static async Task SendAsync()
    {
        var model = new SmsModel
        {
            Body = "سلام",
            Numbers = new[] { "0917*******" }
        };
    
        var service = new SmsService(model);
    
        await service.SendSmsAsync();
    }

    • #
      ‫۷ سال و ۵ ماه قبل، یکشنبه ۳ اردیبهشت ۱۳۹۶، ساعت ۲۱:۵۲
      به شکل فعلی آن تقلبی هست. اصل این فرآیند مربوط است به متد وب سرویس SendGroupSmsSimple و چون تبادلات شبکه‌ای دارد (IO-bound)، باید نگارش async آن توسط ارائه کننده‌ی آن فراهم شود تا واقعا مفهوم async را پیاده سازی کند. در اینجا شما فقط یک thread اضافه‌تر ایجاد کرده‌اید (CPU-bound operation) و ایجاد یک thread به معنای وجود یک عملیات async نیست.
  • #
    ‫۵ سال و ۳ ماه قبل، چهارشنبه ۱۵ خرداد ۱۳۹۸، ساعت ۱۷:۲۳
    در این مثال عملیات تبدیل sync به async درست انجام شده ؟
    کد sync :
            public string AESEncrypt(string Text)
            {
                byte[] dataToEncrypt = Encoding.UTF8.GetBytes(Text);
                byte[] key = Convert.FromBase64String(AES_Key);
                byte[] iv = Convert.FromBase64String(AES_Iv);
    
                using (var aes = new AesManaged())
                {
                    aes.Mode = CipherMode.CBC;
                    aes.Padding = PaddingMode.PKCS7;
                    aes.Key = key;
                    aes.IV = iv;
                    using (var ms = new MemoryStream())
                    {
                        using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
                        {
                            cs.Write(dataToEncrypt, 0, dataToEncrypt.Length);
                            cs.FlushFinalBlock();
                            return Convert.ToBase64String(ms.ToArray());
                        }
                    }
                }
            }
    کد async :
            public async Task<string> AESEncryptAsync(string Text)
            {
                byte[] dataToEncrypt = Encoding.UTF8.GetBytes(Text);
                byte[] key = Convert.FromBase64String(AES_Key);
                byte[] iv = Convert.FromBase64String(AES_Iv);
    
                using (var aes = new AesManaged())
                {
                    aes.Mode = CipherMode.CBC;
                    aes.Padding = PaddingMode.PKCS7;
                    aes.Key = key;
                    aes.IV = iv;
                    using (var ms = new MemoryStream())
                    {
                        using (var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
                        {
                            await cs.WriteAsync(dataToEncrypt, 0, dataToEncrypt.Length);
                            cs.FlushFinalBlock();
                            return Convert.ToBase64String(ms.ToArray());
                        }
                    }
                }
            }
    من این دو متد رو از نظر "زمان اجرا" ، "ترد مشغول" ، "هسته cpu مشغول" در asp.net mvc تست گرفتم {1000 بار فرخوانی} .
    در این تست متد async  هر بار زمان بیشتری برای محاسبه برد (که خوب چون ترد جدید باز میکنه طبیعی هست) و در ترد‌ها و هسته‌های مختلف cpu اجرا میشد ، که این هم طبیعی هست ، طرف نظر از این که طبق مقاله حاضر کلا اینجا async نباید اجرا بشه چون خارج از مرز سیستم نیست ولی عملیات async کامل انجام میشه (فکر میکنم).
    اما متد sync که بررسی شد ، خروجی به صورتی بود که آن هم هر بار در ترد و هسته مختلف سی پی یو اجرا میشد ! سوال دلیل چیست ؟
    مگر تفاوت sync و async  دقیقا به اجرای آنها نیست ؟ پس چرا در هر دو حالت ما اجرا‌های یکسانی داریم ؟
    ممنونم.
      • #
        ‫۵ سال و ۳ ماه قبل، پنجشنبه ۱۶ خرداد ۱۳۹۸، ساعت ۱۶:۰۰
        متشکرم جناب مهندس ، من کل مطالب فارسی راجب به ترد و اسینک و ... چه در دات نت تیپز و چه در سایر وبسایت‌ها و کورس‌ها فارسی چنیدن بار مطالعه کردم ، باز خدا خیرش بده جناب نصیری حداقل در mvc توضیح دادن بقیه که فقط در سی شارپ گفتن و کاملا مشخصه خودشون دقیق نمی‌دونن چی هست ... اما مشکل من با عملی این موضوع هست نه تئوری ، و در پیاده سازی متاسفانه امکان تست صحت کارکرد وجود نداره (قطعا وجود داره ولی منابع نیست) در پایان لینکی که قرار داید ، یه مطلب انگلیسیه خوب ضمیمه شده ولی اون هم تئوری و کلی هست.
        ایا ابزاری وجود داره که صحت درست کارکردن متد‌های async رو بررسی کنه ؟
        ایا مقاله یا ویدو اموزش دارید که به قول خود نویسنده‌های خارجی شیرجه بره در مفهوم async و الگریتم کار رو به چالش بکشه که مشخص کنه دقیقا پشت پرده چکار می‌کنه (واضح باشه) ؟
        ممنونم.
    • #
      ‫۵ سال و ۳ ماه قبل، پنجشنبه ۱۶ خرداد ۱۳۹۸، ساعت ۱۶:۵۴
      «...در این تست متد async  هر بار زمان بیشتری برای محاسبه برد (که خوب چون ترد جدید باز میکنه طبیعی هست)...»
      - زمان بیشتر بردن کاملا طبیعی هست؛ چون متد async یک Coroutines را در پشت صحنه ایجاد می‌کند (کدهای قسمتی از آن توسط کامپایلر به این صورت تولید می‌شود):
      و این Coroutines نسبت به حالت sync، تعداد سیکل CPU بیشتری را مصرف می‌کنند. اما نکته‌ی اصلی این است که عملیات async ترد جدیدی هم ایجاد نمی‌کند (دلیل اصلی علاقه‌ی به آن این است و نه اینکه تعداد سیکل CPU کمتری دارد). بحث اصلی ما بالا رفتن میزان «مقیاس‌پذیری» سیستم هست و نه بالا رفتن سرعت اجرای قطعه کدی خاص که مطلقا چنین چیزی نیست.
      + متد WriteAsync برای نوشتن در FileStream (در استریم فایل سیستم) و یا نوشتن در استریم یک سوکت شبکه هست (IO-bound operations) و نه نوشتن در حافظه (MemoryStream) به صورت async که حاصلی ندارد (خلاصه‌ی بحث جاری یا انجام عملیاتی CPU-bound operation). اهمیت نوشتن async در فایل سیستم و یا استریم سوکت شبکه هم در همان مطلب «اعمال غیر همزمان و چند ریسمانی» بحث شده و همچنین سایر قسمت‌های این سری. بحث متدهای Async اضافه شده به دات نت فریم ورک، ربطی به مباحث چند ریسمانی ندارد «... متدهای Async واقعی کار با شبکه و اعمال I/O، از ترد استفاده نمی‌کنند ...». به همین جهت نسبت به حالت استفاده از تردها سربار کمتری دارند.  
      • #
        ‫۵ سال و ۳ ماه قبل، دوشنبه ۲۰ خرداد ۱۳۹۸، ساعت ۰۴:۲۴
        ایا روشی برای تست تقلبی بودن و یا تقلبی نبودن متدهای async وجود دارد ؟
        یا به عبارتی چطور متوجه شویم متدی که ساختیم تا از جایی گرفتیم ( مثلا از stackowerflow برداشتیم) یه متد async واقعی است یا نه ؟
        ممنونم .
        • #
          ‫۵ سال و ۳ ماه قبل، دوشنبه ۲۰ خرداد ۱۳۹۸، ساعت ۰۴:۵۰
          حذف شدی از سایت. بعد از این همه توضیح؟ .. مغرضی.
  • #
    ‫۲ سال و ۳ ماه قبل، شنبه ۲۴ اردیبهشت ۱۴۰۱، ساعت ۱۵:۱۱
    برای کار با دستگاه هایی که اتصال‌ها بر پایه IntropCom هست به چه شکلی میتوان فرایند async را انجام داد؟ مثلا دستگاهی که نیاز به انجام کانکت و سایر دستور العمل‌ها دارد که همین کانکت کردن مدت زمانی طول میکشد مانند متد زیر 
    device.connect(ip,port)

    با توجه به اینکه دستور العمل در خارج از مرزهای اپلیکیشن انجام می‌گیرد امکان ایجاد یک متد غیرهمزمان وجود دارد؟