Deeper 🫟 DotNet
92 subscribers
1 file
4 links
Download Telegram
Channel created
سلام به دوستایی که لطف کردن جوین دادن و یا جوین خواهند داد 😁
پیش از هر چیزی بگم اینجا دنبال تکرار مکررات و پست‌های معروف "لینکدینی" نیستیم 😅.
در اینجا دنبال یکسری نکات و الگو‌های مهم و یا شرح CLR به صورت عمیق هستیم.
البته که خود من هم مثل هر مهندس نرم‌افزاری در حال یادگیری مستمر هستم پس اگر جایی اشتباهی دیدید شاید حق با شما باشه.

🔸️ امشب برای شروع کار چنل یک پست آماده کردم که در زمینه Concurrency و کار با Task یک نکته‌ی مهم رو ارائه میده که ساده ولی پر اهمیته.
پس از دستش ندید ✌️🏻
🔥75
#Concurrency
سلام به همه 😍
همانطور که می‌دانید یکی از روش‌های ایجاد تسک در دات نت استفاده از کلاس Task به شکل زیر می‌باشد:
 Task.Run()

اما اگر کمی در ریپازیتوری‌ها نگاه کرده‌ باشید استفاده از Task.Factory را احتمالا دیده باشید:
Task.Factory.StartNew(
action: () => DoWork(),
creationOptions: TaskCreationOptions.LongRunning);

امروز در این پست می‌خوایم یک نگاه عمیق به قسمت TaskCreationOptions.LongRunning داشته باشیم و ببینیم دقیقا چه تفاوتی با روش مرسوم دارد.

پیش از اینکه بریم سراغ اصل مطلب باید یک نگاهی از بالاتر به اتفاقی که در زمان ایجاد Task در Runtime رخ می‌دهد داشته باشیم.

📌 همانطور که از درس‌های سیستم‌عامل به یاد داریم کوچک‌ترین واحد اجرایی در سطح کرنل Thread هست. در دات نت به منظور اینکه سربار ایجاد کردن و مدیریت Thread از روی دوش توسعه‌دهنده برداشته شود یک لایه انتزاعی به Threading افزوده شده که ما از طریق Task با آن کار می‌کنیم. در واقع به لطف این لایه در دات نت Task یک انتزاع از کار بر روی Thread می‌باشد و این اجازه را به ما می‌دهد که با دغدغه و پیچیدگی کمتری به توسعه کدهای Asynchronous بپردازیم.

📌 این لایه انتزاعی از یک عضو بسیار مهم یعنی ThreadPool تشکیل شده. کار این عضو مهم این هست که از پیش یکسری Worker Thread برای اجرای پروسس ما از سیستم‌عامل قرض گرفته و آماده به کار نگه دارد تا زمانی که Task جدیدی را تعریف می‌کنیم از طریق یک Global Queue به این Worker Threads داده تا اجرای آن را بر عهده بگیرد.

⁉️خب اینکه دقیقا چه تعداد Worker Thread برای پروسس ما لازم هست، Runtime از طریق یک الگوریتم، به اسم Hill Climbing، در بازه زمانی 500ms رصد می‌کند و تعداد این Worker Threads را مدیریت می‌کند (بعدا در موردش کلی صحبت می‌کنیم)

🔸پس هر زمانی که ما یک Task ایجاد می‌کنیم، در صف ThreadPool قرار می‌گیرد و محض اینکه یک Thread در وضعیت Idle باشد اجرای آن را بر عهده می‌گیرد.

📌 مطابق Best Practice برای اینکه در عملکرد الگوریتم Hill Climbing اختلالی ایجاد نکنیم، نباید در داخل Task.Run از کدهای Blocking و یا CPU Intensive استفاده کنیم. چرا؟ چون که به محض بلاک کردن Worker Thread الگوریتم Hill Climbing وضعیت Throughput Peak شناسایی می‌کند و سراغ کرنل می‌رود تا Worker Thread جدید از سیستم‌عامل قرض بگیرد.
چند نمونه از کدهای Blocking شامل کدهای زیر هست:
 
innerTask.Result;
CpuIntensiveCode();
Thread.Sleep(1000);

🔸مشکل این کار چیه ⁉️ مشکل از جایی شروع می‌شود که ما ThreadPool را با این روش مسموم کنیم. چرا که شروع به بلعیدن Thread از سیستم‌عامل کرده و با توجه به سربار حافظه و پردازش که ایجاد می‌شود، پروسس ما ممکن است دچار OOM شود و یا عملکرد آن به شدت افت کند. حتی اگر چنین اتفاقی رخ ندهد هر Thread به صورت تقریبی بین 1MB تا 8MB بسته به سیستم‌عامل میزبان منابع به صورت Reservation مصرف خواهد کرد.

🔸ولی آیا این به معنی این هست که نباید هرگز از کدهای Blocking استفاده کنیم؟ به طور مشخص برای این کار روش‌های مشخصی وجود دارد. به عنوان مثال مدیریت Thread خارج از ThreadPool به صورت مستقیم که چالش‌های خودش را دارد و یا استفاده از کدی که در ابتدای پست آوردم یعنی:

Task.Factory.StartNew(
action: () => DoWork(),
creationOptions: TaskCreationOptions.LongRunning);

💯 زمانی که ما با این روش یک Task ایجاد می‌کنیم، به Runtime می‌گوییم که این Task را بر روی یک Dedicated Thread خارج از ThreadPool اجرا بکن. پس در واقع مشکل مسموم شدن ThreadPool از بین رفته و بدون درگیر شدن با مدیریت Thread کدهای Blocking خودمان را اجرا کردیم و پس از اتمام Thread که در اختیار ما بوده به کرنل بازگردانده می‌شود بدون آنکه ThreadPool درگیر این فرآیند شده باشد.

📌اما این پست با هدف رسیدن به یک نکته‌ی بسیار مهم ایجاد شده یعنی جایی که من بارها در کدبیس‌های مختلف دیدم.
کد زیر را در نظر بگیرید:
Task.Factory.StartNew(
action: async () => {
DoCpuIntensiveWork();
await Task.Delay(1000);
},
creationOptions: TaskCreationOptions.LongRunning);

📌 خب Long Running Task تا جایی که به اولین Await برسد مطابق چیزی که گفتیم کارش را انجام می‌دهد. به محض اینکه با اولین Await رو به رو می‌شود مطابق رفتار استاندارد Runtime مجددا Task داخلی یعنی Task.Delay(1000) برای اجرا به داخل ThreadPool فرستاده خواهد شد و ما اینجا به ضرر پروسس عمل کرده‌ایم. چرا که یک Thread از سیستم‌عامل گرفتیم و کارش را دادیم به یک Worker Thread در داخل ThreadPool و یا Context یعنی برگشتن به خونه‌ی اول!
پس به عنوان کلام آخر Async/Await + Long Running Task ممنوع 🚫
6👍4💯1
Deeper 🫟 DotNet
#Concurrency سلام به همه 😍 همانطور که می‌دانید یکی از روش‌های ایجاد تسک در دات نت استفاده از کلاس Task به شکل زیر می‌باشد: Task.Run() اما اگر کمی در ریپازیتوری‌ها نگاه کرده‌ باشید استفاده از Task.Factory را احتمالا دیده باشید: Task.Factory.StartNew( action:…
✅️ نکته‌ی پایانی:
اگر به دنبال Task هستیم که در داخلش کار Blocking انجام بدیم نباید همزمان در داخلش هم سراغ Await/Async بریم چون عملا داریم کار را خراب می‌کنیم‌.
پس Regular Task را هرگز به صورت LongRunning اجرا نکنید.
👍61
سلام به همگی ✌️🏻
پست بعدی میریم سراغ AsyncLocal یک نگاه عمیق به Life Cycle داشته باشیم و اینکه چطور در داخل ExecutionContext مدیریت میشه.
خیلی جذابه مکانیزمش.
منتظرش باشید ❤️
🔥61
سلام وقت همگی بخیر ❤️
یکی از دوستان پیشنهاد داد برای پیش‌زمینه فنی پست‌ها یکسری رفرنس بدم برای مطالعه بیشتر.
در این رابطه من برای مباحث #Concurrency این چند تا لینک رو به ترتیب مطالعه قرار می‌دهم.

1- https://medium.com/net-under-the-hood/internal-mechanisms-of-tasks-in-net-ef461956d4a7

2- https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming

به نظرم این دو تا لینک به خوبی دید میده بهتون.
سعی می‌کنم بیشتر موارد حرفه‌ای تر رو اینجا پوشش بدم ولی پیش‌زمینه‌اش رو هم بچینم.
🔥51👍1
Concurrency_in_C#_Cookbook,_2nd_Edition_by_Stephen_Cleary_OReilly.pdf
8.4 MB
#Concurrency
📚 این کتاب رو خیلی پیشنهاد می‌کنم بدون توضیح اضافه به ارایه Best practices در زمینه Concurrency پرداخته.
فقط در نظرتون باشه Cookbook هست یعنی خیلی توضیحات عمیقی نمیده ولی خوبه دم دستتون باشه و نگاه بکنید ❤️
💯64
دوست دارید یک سلسله پست هم در زمینه Allocation Reduction و استفاده از Profiler در ویژوال استودیو درست کنم؟
ولی مشخصا باید به صورت ویدیو باشه.
👍122
#Concurrency
سلام 😍
تو این پست می‌خواهم به یکی از امکانات دات نت در کدهای Asynchronous یعنی AsyncLocal🔗 بپردازم؛ بریم برای چیدن مقدمه.

🔸 خب تا حالا فکر کردید چطور می‌تونیم یک فیلد ساده مثل TraceId درخواست را بین کدهای Async/Await انجام بدیم؟ احتمالا اول به سراغ آرگومان می‌روید مثل زیر:

var traceId = 18902;
var result = await DoSomething(traceId);


🔸خب حالا فرض کنید در داخل DoSomething هم بخواهیم این کار رو مجدد انجام بدیم یعنی دوباره این TraceId را پاس کاری کنیم. خب سوال اول چه تضمینی هست در یک جایی این TraceId را تغییر ندیم؟ مثلا مقدارش رو تغییر بدیم یا اشتباهی جای یک آرگومان دیگر پاس بدیم؟ شاید بگید با یک Immutability🔗 کار رو انجام می‌دهیم مثل Record.
var traceObj = new Trace(18902);
var result = await DoSomething(traceObj);

🔸حالا فرض کنید این کدبیسی که می‌نویسیم بزرگتر بشود. آن وقت باید همیشه به عنوان یک Design Decision یاد تیم باشه که باید آرگومان Trace را تعریف بکند. یعنی بار ذهنی بیشتری روی تیم می‌اید و ممکنه خطای انسانی رخ بده و یا در جایی این اصل را فراموش بکنیم و کدبیس در مسیر اشتباه پیش برود.

🔸حالا بعد از مدتی ما تصمیم بگیریم فیلدهای دیگری را هم اضافه بکنیم. اینجاست که کل کدبیس رو باید دست خوش تغییر بکنیم. یعنی به صورت مفهومی کدبیس ما وابسته به آرگومان شده و دچار Ripple Effect🔗 خواهد شد.

⁉️ممکنه تو ذهن شما راه‌های بسیاری برای حل این موضوع بیاد مثل یک سرویس Singleton یا Static Class ولی مدیریت این‌ها در محیط Concurrent چالش‌های خودش را دارد. این جا هست که دات نت برای ما یک امکانی را فراهم کرده.

این امکان یعنی AsyncLocal به شکل زیر هست:
public static class Trace{
private static readonly AsyncLocal<int> _traceId = new();

public static int TraceId
{
get => _traceId.Value;
set => _traceId.Value = value;
}
}

public static async Task Main()
{
Trace.TraceId = 12809;
var result = await DoSomething();
}

public async Task DoSomething(){
var traceId = Trace.TraceId;
await DoAnotherThing();
}

public async Task DoAnotherThing(){
var traceId = Trace.TraceId;
await DoLast();
}

⁉️خب اما فرق این با یک کلاس ساده و فیلد داخلش چیه؟
🔸قضیه این هست که AsyncLocal فراتر از یک Static Field هست و به شما تضمین می‌دهد که مقدار TraceId در بین توالی از Async/Await حفظ بشود و در کل مسیر منطقی اجرایی (Unit of Work🔗) همه‌ی Consumers یک نسخه از آن را ببیند. بر خلاف Static Fields که بین همه‌ی Threads مقداری یکسان دارد، AsyncLocal این مقدار را بر روی هر 🔗ExecutionContext مجزا نگهداری می‌کند.
💯همچنین به واسطه پترن ساده‌ای که دارد می‌تونید به سادگی مثل یک کلاس Context در کدبیس تعریف کنید بهش دسترسی داشته باشید. البته این AsyncLocal هزینه Performance و یک نکته مهم در زمینه Memory Leak🔗 دارد که اشاره می‌کنم.

📌نکته Performance این هست که AsyncLocal به منظور حفظ Immutability🔗 و محافظت از Memory Leak🔗 در زمان Write از کل AsyncLocal مورد استفاده Clone می‌گیرد پس برای سناریو‌های Many Write اصلا مناسب نیست ولی برای Many Read هیچ مشکلی ندارد.

📌نکته Memory Leak🔗 هم این هست که به منظور جلوگیری از Leak شدن مقدار پس از پایان کار باید حتما مقدار داخل AsyncLocal ریست بشه. علتش؟ بعدا در پستی جدا که رفتیم در دل AsyncLocal در موردش مفصل صحبت می‌کنم.
پس یعنی باید چنین کاری بکنیم:
public static class Trace{
private static readonly AsyncLocal<int> _traceId = new();

public static int TraceId
{
get => _traceId.Value;
set => _traceId.Value = value;
}

public static void Reset()
{
_traceId.Value = default;
}
}

public static async Task Main()
{
Trace.TraceId = 12809;
var result = await DoSomething();
//We are done with work
Trace.Reset()
}

public async Task DoSomething(){
var traceId = Trace.TraceId;
await DoAnotherThing();
}

public async Task DoAnotherThing(){
var traceId = Trace.TraceId;
await DoLast();
}

اما بریم برای بخش دوم پست که یک نکته طلایی دارد؛
4
😍💯 اما یک نکته‌ی طلایی🥇 که کمتر جایی بهش اشاره کرده. به نظر شما مقدار خروجی کد زیر چی خواهد بود؟

public static async Task Main()
{
Trace.TraceId = 12809;
var result = await DoSomething();
Console.WriteLine("Main "+Trace.TraceId);
//We are done with work
Trace.Reset()
}

public async Task DoSomething(){
Trace.TraceId = 101010
Console.WriteLine("DoSomething "+Trace.TraceId);
await DoAnotherThing();
}

public async Task DoAnotherThing(){
var traceId = Trace.TraceId;
Console.WriteLine("DoAnotherThing "+traceId);
await DoLast();
}

بر خلاف تصور شما خروجی زیر را خواهیم دید:
DoSomething 101010
DoAnotherThing 101010
Main 120809

مکانیزم داخلی AsyncLocal اجازه انتشار تغییرات رو از فرزند به پدر نمی‌دهد! در واقع در زمان ورود به یک Async/Await از مقدار پدر کپی گرفته و در داخل یک ExecutionContext دیگر در اختیار فرزند می‌گذارد و تغییرات فرزند فقط در داخل ExecutionContext خود فرزند رخ می‌دهد. حال این اتفاق در فرزند فرزند هم عینا رخ می‌دهد. پس AsyncLocal یک one-way flow را برای ما فراهم می‌کند.

📌جمع‌بندی کنیم:
🔸از AsyncLocal در سناریوهای Single Write / Many Read استفاده کنید.
🔸بعد از اتمام کار حتما Reset کنید تا State leak پس از پایان UoW اتفاق نیافتد.
🔸مسیر تغییرات همیشه از پدر به فرزند هست و نه برعکس.
11
راستی در داخل پست روی کلمات کلیدی لینک گذاشتم حتما مطالعه کنید❤️
3
#Collection
سلام به همگی 👋
امروز یک نکته مهم و کاربردی ولی ساده درباره کار با Dictionary می‌خواهم با شما به اشتراک بگذارم.

وقتی در Dictionary یک نوع داده را به عنوان کلید تعریف می‌کنیم، دیکشنری برای یافتن مقدار مورد نظر از ترکیب ()GetHashCode و ()Equals استفاده می‌کند.

🔹 برای string و نوع‌های مقداری ساده مثل int، هیچ مشکلی وجود ندارد چون این نوع‌ها بهینه‌سازی شده‌اند و باکسینگ اتفاق نمی‌افتد.
🔹 اما وقتی از struct به عنوان کلید استفاده می‌کنیم، اگر <IEquatable<T پیاده‌سازی نشده باشد، متد Equals(object) فراخوانی می‌شود و struct به object باکس می‌شود. این کار باعث تخصیص اضافی حافظه خواهد شد ⚠️

راه‌حل این است که برای structهایی که قرار است کلید باشند، همیشه <IEquatable<T را پیاده‌سازی کنیم:
public struct KeyType : IEquatable<KeyType>
{
public string Val { get; init; }

public bool Equals(KeyType other) =>
Val == other.Val;

public override bool Equals(object obj) =>
obj is KeyType other && Equals(other);

public override int GetHashCode() =>
Val?.GetHashCode() ?? 0;
}

با این کار دیکشنری مستقیماً از متد جنریک Equals(KeyType) استفاده می‌کند و دیگر هیچ باکسینگ غیرضروری رخ نخواهد داد 🚀
10
پست بعدی دوست دارید در چه زمینه باشه؟
Final Results
73%
Concurrency
27%
Garbage Collection
1
سلام به همگی ❤️
ببخشید که سر زمانبندی پست‌ها بدقولی شد. حسابی درگیر پروژه شخصی بودم.
ولی این هفته فعالیت چنل رو از سر میگیرم.
میخوام یک پست جمع و جور درباره Task Lifecycle بذارم که راه برای ادامه مسیر Concurrency هموار باشه.
6👍3