Async and Parallel Programming in C# - Part 6
در این ویدیو، سعی بر توضیحات اصلی مفاهیم پارالل پروگرمینگ و ایسینگ پروگرمینگ هست تمرکز کردیم و 2 تا مثال در دنیای واقعی و مثالی در دنیای کد رو هم بررسی کردیم
01:45 Async and Parallel Programming
10:20 Real-World Example of Sync and Async
17:11 Demo
مدت زمان ویدیو : 24 دقیقه
معرفی Scala برای توسعهدهندگان #C
Scala is a general purpose programming language designed to express common programming patterns in a concise, elegant, and type-safe way. It smoothly integrates features of object-oriented and functional languages, enabling Java and other programmers to be more productive. Code sizes are typically reduced by a factor of two to three when compared to an equivalent Java application.
In this part, we provide a list of VPS providers that you can use it to buy a vps.
We do not include cheap VPS with less than 0.5GB RAM and less than 12 months in this list. The price in this list is the average of 12 month. In addition, we don't include a VPS provider here, if the price is higher than well known providers such as Hetzner, Ovh, DigitalOcean, AWS, Azure.
کتابخانه sysend.js
Tested on GNU/Linux in Chromium 34, FireFox 29, Opera 12.16 (64bit)
نوعهای نال نپذیر در TypeScript
در نگارش 2.7 اگر یک چنین تعریفی را داشته باشید:
export class MovieComponent { @Input() movie: Movie; }
Error! Property movie has no initializer and is not assigned directly in the constructor.
@Input() movie: Movie | null = null;
class C { baz: boolean | undefined; }
class C { bar = "hello"; }
class C { foo!: number; ngOnInit() { this.foo = 0; } }
زمانیکه تصمیم میگیریم کدهای زده شده را بهینه کنیم، اکثرا دنبال راه حلهای جدید نمیگردیم. این مورد کاملا غریزی است؛ چرا که بهدنبال کمترین انرژی و بیشترین بازدهی هستیم؛ این طبیعت انسان است. صرفا کدهای قبلی را بازبینی میکنیم و سعی میکنیم نحوهی نوشتن منطقهای موجود را بهینه کنیم. در همین راستا درک عملکرد Task و ValueTask ها شاید قدمی مهم در مورد بهینه کردن کدها باشد؛ چرا استفاده درست و بجای این دو مورد میتواند تاثیر زیادی بر روی سرعت و استفاده از مصرف حافظه داشته باشد؟ در این مقاله سعی میکنیم تا درک درستی از این دو داشته باشیم.
Task<T> چیست؟
Task یک کلاس در فضای نام System.Threading.Tasks است؛ بهطوریکه کمک میکند تا یک قسمت از برنامه به صورت مستقل از Thread اصلی اجرا شود. بهبیان دیگر میتواند یک Thread Pool را ایجاد و با توجه به روند کار، از یک مرحلهی اجرایی به مرحلهای دیگر منتقل میکند. همچنین هر Task میتواند یک مقدار برگشتی نیز داشته باشد.
این درحالیاست که میتواند صرفا یک فرآیند را اجرا کند، بدون اینکه خروجی داشته باشد. بهعبارتی دیگر اگر فرآیندی داشته باشیم که در نهایت یک شناسه را برمیگرداند، از Task<int> و اگر فرآیندی داشته باشیم که صرفا فرآیند همگام سازی دادههای قدیمی به جدید را انجام میدهد، میتواند از نوع Task باشد.
همانطور که اشاره شد، Task یک کلاس است که شامل متدها و فیلدهای مختلفی میباشد. با استفاده از این اعضا میتوان نحوهی اجرای کدها و وضعیتهای مختلف اجرای آن را مدیریت کرد، تا در نهایت اجرای آن کامل شود.
به دلیل اینکه Task یک class است و class ها از نوع ReferenceType میباشند، روی حافظهی Heap ذخیره میشوند و بهازای هر بار فراخوانی متدی که خروجی Task دارد، شیء Task را روی Heap ذخیره میکند. این شیء وضعیت اجرای قسمتی از کد ما را که میتواند sync یا async باشد، در خود ذخیره میکند تا در نهایت اجرای آن کامل شود.
نحوه استفاده از Task<T>
برای درک بهتر، یک تکه کد را با بهره بردن از Task ایجاد میکنیم :
public static class DummyWeatherProvider { public static async Task<Weather> Get(string city) { await Task.Delay(10); var weather = new Weather { City = city, Date = DateTime.Now, AvgTempratureF = new Random().Next(5, 70) }; return weather; } }
static async Task CheckTaskStatus() { var task = DummyWeatherProvider.Get("Stockholm"); LogTaskStatus(task.Status); await task; LogTaskStatus(task.Status); } static void LogTaskStatus(TaskStatus status) { Console.WriteLine($"Task Status: {Enum.GetName(typeof(TaskStatus), status)}"); }
ValueTask<T> چیست؟
همانند Task ، ValueTask هم برای مدیریت وضعیت فرآیند استفاده میشود؛ با این تفاوت که ValueTask ها از نوع struct هستند. بهطوریکه نحوهی ذخیره سازی آنها در حافظه به نسبت class ها کاملا متفاوت است. از نقطه نظر سرعت، تشخیص دادن اینکه کدامیک باید استفاده شود، باید با توجه به سناریو، بررسی و انتخاب شود؛ چرا که از نظر تخصیص حافظه متفاوت عمل میکنند. برای درک بهتر عملکرد ValueTask ها کد زیر را بررسی میکنیم :
public class WeatherService { private readonly ConcurrentDictionary<string, Weather> _cache; public WeatherService() { _cache = new(); } public async Task<Weather> GetWeatherTask(string city) { if (!_cache.ContainsKey(city)) { var weather = await DummyWeatherProvider.Get(city); _cache.TryAdd(city, weather); } return _cache[city]; } public async ValueTask<Weather> GetWeatherValueTask(string city) { if (!_cache.ContainsKey(city)) { var weather = await DummyWeatherProvider.Get(city); _cache.TryAdd(city, weather); } return _cache[city]; }
کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده میکنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد GetWeatherTask و GetWeatherValueTask کاملا شبیه به هم است؛ بهطوریکه اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده میشود؛ در غیر این صورت DummyWeatherProvider.Get() فراخوانی خواهد شد.
در قدم بعدی اطلاعات بهدست آمده را در _cache ذخیره میکنیم. سپس مقدار ذخیره شده را برگشت میدهیم. در واقع تنها تفاوت دو متد ذکر شده، نوع خروجی آن میباشد؛ یکی از Taskو دیگری از ValueTask استفاده میکند.
برای مقایسهی مصرف حافظهی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کدهای زیر داریم :
[MemoryDiagnoser] public class TaskAndValueTaskBenchmark { private readonly WeatherService _weatherService; public TaskAndValueTaskBenchmark() { _weatherService = new(); } [Benchmark] [Arguments("Denver")] public async Task<Weather> TaskBenchmark(string city) { return await _weatherService.GetWeatherTask(city); } [Benchmark] [Arguments("London")] public async ValueTask<Weather> ValueTaskBenchmark(string city) { return await _weatherService.GetWeatherValueTask(city); } }
نتیجه به دست آمده به شرح زیر است :
Allocated | Gen0 | Method |
144 B | 0.0229 | TaskBenchmark |
------ | ---- | ValueTaskBenchmark |
مزیت ValueTask<T>
بهدلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره میشوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف میشوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش میدهد .
علاوه بر این، در سناریویی که اکثر کدها به صورت sync اجرا میشوند، در این مواقع استفاده از ValueTask، بهتر از Task میباشد .
این سری متد GetWeatherValueTask
را جهت تشخص اینکه اغلب کدها به صورت sync یا async اجرا میشوند، بررسی میکنیم. در
متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا میشود و اگر شهر وجود
نداشته باشد، کار به صورت async اجرا میشود. با بررسی دقیقتر متوجه میشویم اکثر مواقع در این متد کار به صورت sync
اجرا میشود؛ چرا که بعد ازدریافت
اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان _cache ) .
محدودیتهای استفاده از ValueTask<T>
1. در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await میکنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.
2. اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).
3. نمیتوانیم به یک ValueTask به صورت هم زمان در حالت Multi threads دسترسی داشته باشیم.
4. به صورت پیش فرض خروجی عملیات async، نوع Task میباشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.
منابع :
using System; using System.Net; namespace Async15 { class Program { static void Main(string[] args) { using (var webClient = new WebClient { }) { webClient.Headers.Add("User-Agent", "AsyncContext 1.0"); var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir"); Console.WriteLine(data); } } } }
اضافه کردن واژهی کلیدی async به روالهای رخدادگردان void برنامههای دسکتاپ مجاز است؛ با توجه به اینکه متد async پیش از پایان کار به فراخوان بازگشت داده میشوند (ذات متدهای async به این نحو است). در برنامههای دسکتاپ، این بازگشت به UI event loop است؛ بنابراین برنامه بدون مشکل به کار خود ادامه خواهد داد. اما در اینجا، بازگشت متد Main، به معنای بازگشت به OS است و خاتمهی برنامه. به همین جهت کامپایلر از async کردن آن ممانعت میکند.
برای حل این مشکل در برنامههای کنسول و همچنین برنامههای سرویس ویندوز NT که دارای یک async-compatible context نیستند، میتوان از یک کتابخانهی کمکی سورس باز به نام Nito AsyncEx استفاده کرد. برای نصب آن دستور ذیل را در کنسول پاورشل نیوگت وارد کنید:
PM> Install-Package Nito.AsyncEx
using System; using System.Net; using Nito.AsyncEx; namespace Async15 { class Program { static void Main(string[] args) { AsyncContext.Run(async () => { using (var webClient = new WebClient()) { webClient.Headers.Add("User-Agent", "AsyncContext 1.0"); var data = await webClient.DownloadStringTaskAsync("https://www.dntips.ir"); Console.WriteLine(data); } }); } } }
async lambda
در مثال فوق از یک async lambda، برای فراخوانی استفاده شده است که به همراه دات نت 4.5 ارائه شدهاند:
Action, () => { } Func<Task>, async () => { await Task.Yield(); } Func<TResult>, () => { return 13; } Func<Task<TResult>>, async () => { await Task.Yield(); return 13; }
روش دوم استفاده از AsyncContext.Run و مقدار دهی Func of Task، تعریف یک متد مستقل async Task دار، به نحو ذیل است:
class Program { static async Task<int> AsyncMain() { .. } static int Main(string[] args) { return AsyncContext.Run(AsyncMain); } }
رخدادهای مرتبط با طول عمر برنامه را async تعریف نکنید
همانند متد Main که async تعریف کردن آن سبب بازگشت آنی روال کار به OS میشود و برنامه خاتمه مییابد، روالهای رخدادگردانی که با طول عمر یک برنامهی UI سر و کار دارند مانند Application_Launching، Application_Closing، Application_Activated و Application_Deactivated (خصوصا در برنامههای ویندوز 8) نیز نباید async void تعریف شوند (چون مطابق ذات متدهای async، بلافاصله به برنامه اعلام میکنند که کار تمام شد). در این موارد خاص نیز میتوان از متد AsyncContext.Run برای انجام اعمال async استفاده کرد.
خلاصه اولین جلسه طراحی زبان C# 7
Highlights:
- Representing data better in code (tuples, object destructuring, pattern matching, record types, array slices)
- Metaprogramming (virtual extension methods, default interface implementations, enhanced generic constraints, mixins/traits, delegation)
- Immutable values (readonly var x ~ val x ~ let x)
- Asynchronous enumeration and streams
- Code contract language integrations
- Structural typing (think implicit interfaces in Go)
- Explicit lambda capture handling (pass by value or reference semantics explicitly)
- Solve null reference problem (Non-nullable references, Option type)