C#: البرمجة غير المتزامنة
1. التزامني مقابل غير التزامني
يتم تنفيذ الكود المتزامن سطراً سطراً بالترتيب. وعندما يواجه عملية تستغرق وقتاً طويلاً، فإنه يحجب الخيط الحالي، مما يجعل البرنامج لا يستجيب. أما الكود غير المتزامن فيحرر الخيط أثناء انتظار العمليات التي تستغرق وقتاً طويلاً، مما يسمح للبرنامج بمواصلة أداء مهام أخرى.
مشاكل الحجب المتزامن:
- خيط واجهة المستخدم مشغول، مما يؤدي إلى تجميد الواجهة وعدم استجابتها
- تُهدر خيوط الخادم في انتظار عمليات الإدخال/الإخراج، مما يقلل من معدل الإنتاجية
- يواجه المستخدمون فترات انتظار أطول، مما يؤدي إلى تجربة سيئة
مزايا النمط غير المتزامن:
- العمليات التي تتطلب استخدامًا مكثفًا لعمليات الإدخال/الإخراج (الشبكة، الملفات، قواعد البيانات) لا تؤدي إلى حجب الخيوط
- يمكن تحويل العمليات التي تستهلك موارد كبيرة من وحدة المعالجة المركزية (CPU) إلى مجموعة الخيوط، مما يحافظ على استجابة واجهة المستخدم
- تحسين التزامن وسعة معالجة البيانات للتطبيقات
▶ مثال
using System;
using System.Threading;
Console.WriteLine("Sync start: " + DateTime.Now.ToString("HH:mm:ss"));
Thread.Sleep(2000);
Console.WriteLine("Sync end: " + DateTime.Now.ToString("HH:mm:ss"));
Sync start: 10:00:00
Sync end: 10:00:02
2. المهمة والمهمة<T>
يمثل Task عملية غير متزامنة لا تُرجع نتيجة. ويمثل Task<T> عملية غير متزامنة تُرجع نتيجة من النوع T. وهذان هما النوعان الأساسيان في البرمجة غير المتزامنة بلغة C#.
| النوع | الوصف | مثال |
|---|---|---|
Task |
عملية غير متزامنة بدون قيمة إرجاع | Task.Delay(1000) |
Task<T> |
عملية غير متزامنة ذات قيمة إرجاع | Task.FromResult(42) |
▶ مثال
using System;
using System.Threading.Tasks;
Task noResultTask = Task.Delay(500);
Task```<int>``` resultTask = Task.FromResult(42);
await noResultTask;
int value = await resultTask;
Console.WriteLine("Retrieved value: " + value);
Retrieved value: 42
3. صيغة async/await
يُشير المُعدِّل async إلى أن الطريقة غير متزامنة، بينما ينتظر المُشغِّل await اكتمال العملية غير المتزامنة دون حجب الخيط. ولا يمكن استخدام await إلا داخل طريقة async.
قواعد النحو:
- تُرجع الدوال
asyncالقيمTaskأوTask<T>أوValueTask<T> awaitيوقف تنفيذ الأسلوب، وينتظر حتى تكتمل المهمة، ثم يواصل التنفيذ- تعود الدالة
asyncإلى المُستدعي عند أولawait
▶ مثال
using System;
using System.Threading.Tasks;
async Task DoWorkAsync()
{
Console.WriteLine("Work start: " + DateTime.Now.ToString("HH:mm:ss.fff"));
await Task.Delay(1000);
Console.WriteLine("Work end: " + DateTime.Now.ToString("HH:mm:ss.fff"));
}
async Task```<string>``` GetMessageAsync()
{
await Task.Delay(500);
return "Async message arrived";
}
await DoWorkAsync();
string msg = await GetMessageAsync();
Console.WriteLine(msg);
Work start: 10:00:00.000
Work end: 10:00:01.000
Async message arrived
4. Task.Run
يقوم Task.Run بتحويل المهام التي تتطلب استخدامًا مكثفًا لوحدة المعالجة المركزية (CPU) إلى مجموعة الخيوط (thread pool) لتنفيذها، مما يمنع حجب الخيط الحالي. وهو مناسب للعمليات التي تتطلب حوسبة مكثفة، وليس لعمليات الإدخال/الإخراج (I/O).
▶ مثال
using System;
using System.Threading.Tasks;
int ComputeHeavy()
{
int result = 0;
for (int i = 0; i < 100_000_000; i++)
{
result += i;
}
return result;
}
Console.WriteLine("Starting computation...");
int total = await Task.Run(() => ComputeHeavy());
Console.WriteLine("Computation result: " + total);
Starting computation...
Computation result: 4999999950000000
5. Task.WhenAll / WhenAny / Delay
| الطريقة | الوصف |
|---|---|
Task.WhenAll |
ينتظر اكتمال جميع المهام |
Task.WhenAny |
ينتظر اكتمال أي مهمة واحدة |
Task.Delay |
ينتظر بشكل غير متزامن لمدة العدد المحدد من الميلي ثانية، ليحل محل Thread.Sleep |
▶ مثال
using System;
using System.Threading.Tasks;
async Task```<string>``` FetchAsync(string site)
{
await Task.Delay(new Random().Next(500, 1500));
return site + " loaded";
}
Task```<string>``` t1 = FetchAsync("SiteA");
Task```<string>``` t2 = FetchAsync("SiteB");
Task```<string>``` t3 = FetchAsync("SiteC");
string[] all = await Task.WhenAll(t1, t2, t3);
Console.WriteLine("--- WhenAll complete ---");
foreach (string s in all)
{
Console.WriteLine(s);
}
Task```<string>``` first = await Task.WhenAny(t1, t2, t3);
Console.WriteLine("First completed: " + first.Result);
--- WhenAll complete ---
SiteA loaded
SiteB loaded
SiteC loaded
First completed: SiteA loaded
6. قواعد تسمية الطرق غير المتزامنة
يجب أن تستخدم أسماء الطرق غير المتزامنة اللاحقة Async. وهذه قاعدة متبعة في .NET تيسر التمييز بين الطرق المتزامنة وغير المتزامنة.
القواعد:
- يجب أن تحمل الطرق التي تُرجع
TaskأوTask<T>اللاحقةAsync - على سبيل المثال،
Read()→ReadAsync()،GetUser()→GetUserAsync() - يتم استبعاد معالجات الأحداث (على سبيل المثال،
Button_Click)
▶ مثال
using System;
using System.Threading.Tasks;
async Task```<string>``` ReadFileAsync(string path)
{
await Task.Delay(200);
return "Read: " + path;
}
async Task SaveDataAsync(string data)
{
await Task.Delay(100);
Console.WriteLine("Saved: " + data);
}
string content = await ReadFileAsync("/data.txt");
await SaveDataAsync(content);
Saved: Read: /data.txt
7. آلية الإلغاء باستخدام CancellationToken
يُستخدم CancellationToken للإلغاء التعاوني للعمليات غير المتزامنة. يقوم المستدعي بإرسال إشارة إلغاء عبر CancellationTokenSource، ويقوم المستدعى إليه بالتحقق من الرمز المميز والإنهاء المبكر.
خطوات الاستخدام:
- إنشاء
CancellationTokenSource - تمرير
cts.Tokenإلى الدالة غير المتزامنة - تقوم الطريقة غير المتزامنة بالتحقق بشكل دوري من
token.IsCancellationRequestedأو تمريرها إلى واجهات برمجة التطبيقات القابلة للإلغاء - اتصل بـ
cts.Cancel()عند الحاجة إلى الإلغاء
▶ مثال
using System;
using System.Threading;
using System.Threading.Tasks;
async Task LongWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine("Step " + (i + 1));
await Task.Delay(300, token);
}
Console.WriteLine("All done");
}
using var cts = new CancellationTokenSource();
cts.CancelAfter(1200);
try
{
await LongWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled");
}
Step 1
Step 2
Step 3
Step 4
Operation cancelled
8. IAsyncEnumerable<T> التدفقات غير المتزامنة
قدمت لغة C# 8 ميزة IAsyncEnumerable<T>، التي تدعم إنشاء واستهلاك تدفقات البيانات بشكل غير متزامن. استخدم await foreach للتكرار غير المتزامن للعناصر، وهو مناسب لسيناريوهات مثل القراءة المجمعة لقواعد البيانات واستلام استجابات الشبكة بشكل متدفق.
▶ مثال
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
async IAsyncEnumerable```<int>``` GenerateNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(200);
yield return i;
}
}
await foreach (int num in GenerateNumbersAsync())
{
Console.WriteLine("Received: " + num);
}
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
9. الأخطاء الشائعة في البرمجة غير المتزامنة
(1) ⚠️ تجنب استخدام async void
لا يمكن تحويل async void إلى await، ولا يمكن التقاط الاستثناءات، وقد يتسبب ذلك في تعطل التطبيق. لا تستخدمه إلا في معالجات الأحداث.
(2) ⚠️ تجنب استخدام .Result و .Wait()
قد يؤدي استدعاء .Result أو .Wait() في سياق واجهة المستخدم (UI) أو ASP.NET إلى حدوث مأزق: فالطريقة غير المتزامنة تنتظر أن يحرر المستدعي الخيط، بينما ينتظر المستدعي اكتمال الطريقة غير المتزامنة — أي أن كلا الطرفين ينتظران بعضهما البعض.
(3) ⚠️ ConfigureAwait(false)
يجب أن يستخدم كود المكتبة ConfigureAwait(false) لتجنب التقاط سياق التزامن، مما يمنع حدوث حالات التعطل ويحسن الأداء.
▶ مثال
using System;
using System.Threading.Tasks;
async Task```<string>``` BadMethodAsync()
{
await Task.Delay(100);
return "Done";
}
async Task SafeUsageAsync()
{
string result = await BadMethodAsync();
Console.WriteLine("Correct usage: " + result);
}
async Task LibraryMethodAsync()
{
string result = await BadMethodAsync().ConfigureAwait(false);
Console.WriteLine("Library usage: " + result);
}
await SafeUsageAsync();
await LibraryMethodAsync();
Correct usage: Done
Library usage: Done
❓ أسئلة شائعة
async void وasync Task؟async void ولا يمكن التقاط الاستثناءات التي قد تحدث فيه؛ لذا يجب استخدامه فقط في معالجات الأحداث. أما async Task فهو النهج القياسي — حيث يمكن انتظار تنفيذه ويمكن معالجة الاستثناءات التي قد تحدث فيه.📖 ملخص
- تعمل الكود المتزامن على حجب الخيوط؛ بينما يعمل الكود غير المتزامن على تحرير الخيوط أثناء الانتظار، مما يحسّن سرعة الاستجابة ومعدل الإنتاجية
Taskتشير إلى عملية غير متزامنة بدون قيمة إرجاع؛Task<T>تشير إلى عملية غير متزامنة بقيمة إرجاعasyncتشير إلى طريقة غير متزامنة؛awaitتنتظر عملية غير متزامنة دون حجب التنفيذTask.Runيُحيل المهام التي تستهلك موارد كبيرة من وحدة المعالجة المركزية (CPU) إلى مجموعة الخيوطTask.WhenAllينتظر اكتمال جميع العمليات؛Task.WhenAnyينتظر اكتمال العملية الأولىTask.Delayهو البديل غير المتزامن لـThread.Sleep- يجب أن تستخدم الطرق غير المتزامنة اللاحقة
Async CancellationTokenيُنفذ الإلغاء التعاوني، ويتم التحكم فيه عبرCancellationTokenSourceIAsyncEnumerable<T>+await foreachيتيح استخدام التدفقات غير المتزامنة- تجنب استخدام
async voidو.Resultو.Wait()؛ واستخدمConfigureAwait(false)في كود المكتبة
📝 تمارين
- اكتب دالة غير متزامنة
DownloadAllAsyncتستخدمTask.WhenAllلتنزيل 3 مصادر بيانات محاكاة في وقت واحد، وتُرجع مصفوفة تحتوي على جميع النتائج - اكتب دالة عد تنازلي غير متزامن تدعم
CancellationToken، وتعرض الثواني المتبقية كل ثانية، وتطبع عبارة "تم إلغاء العد التنازلي" عند الإلغاء - استخدم
IAsyncEnumerable<int>لتوليد أول 20 عددًا من أعداد فيبوناتشي، ثم كرر العملية باستخدامawait foreach، وقم بإخراج كل عدد على حدة - حدد المشكلة في الشفرة التالية وقم بإصلاحها:
async void Button_Click() { var data = GetDataAsync().Result; Console.WriteLine(data); }