C#: البرمجة غير المتزامنة

1. التزامني مقابل غير التزامني

يتم تنفيذ الكود المتزامن سطراً سطراً بالترتيب. وعندما يواجه عملية تستغرق وقتاً طويلاً، فإنه يحجب الخيط الحالي، مما يجعل البرنامج لا يستجيب. أما الكود غير المتزامن فيحرر الخيط أثناء انتظار العمليات التي تستغرق وقتاً طويلاً، مما يسمح للبرنامج بمواصلة أداء مهام أخرى.

مشاكل الحجب المتزامن:

مزايا النمط غير المتزامن:

▶ مثال

CSHARP
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"));
▶ جرّب الكود
TEXT 📖 للعرض فقط
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)

▶ مثال

CSHARP
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);
▶ جرّب الكود
TEXT 📖 للعرض فقط
Retrieved value: 42

3. صيغة async/await

يُشير المُعدِّل async إلى أن الطريقة غير متزامنة، بينما ينتظر المُشغِّل await اكتمال العملية غير المتزامنة دون حجب الخيط. ولا يمكن استخدام await إلا داخل طريقة async.

قواعد النحو:

▶ مثال

CSHARP
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);
▶ جرّب الكود
TEXT 📖 للعرض فقط
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).

▶ مثال

CSHARP
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);
▶ جرّب الكود
TEXT 📖 للعرض فقط
Starting computation...
Computation result: 4999999950000000

5. Task.WhenAll / WhenAny / Delay

الطريقة الوصف
Task.WhenAll ينتظر اكتمال جميع المهام
Task.WhenAny ينتظر اكتمال أي مهمة واحدة
Task.Delay ينتظر بشكل غير متزامن لمدة العدد المحدد من الميلي ثانية، ليحل محل Thread.Sleep

▶ مثال

CSHARP
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);
▶ جرّب الكود
TEXT 📖 للعرض فقط
--- WhenAll complete ---
SiteA loaded
SiteB loaded
SiteC loaded
First completed: SiteA loaded

6. قواعد تسمية الطرق غير المتزامنة

يجب أن تستخدم أسماء الطرق غير المتزامنة اللاحقة Async. وهذه قاعدة متبعة في .NET تيسر التمييز بين الطرق المتزامنة وغير المتزامنة.

القواعد:

▶ مثال

CSHARP
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);
▶ جرّب الكود
TEXT 📖 للعرض فقط
Saved: Read: /data.txt

7. آلية الإلغاء باستخدام CancellationToken

يُستخدم CancellationToken للإلغاء التعاوني للعمليات غير المتزامنة. يقوم المستدعي بإرسال إشارة إلغاء عبر CancellationTokenSource، ويقوم المستدعى إليه بالتحقق من الرمز المميز والإنهاء المبكر.

خطوات الاستخدام:

  1. إنشاء CancellationTokenSource
  2. تمرير cts.Token إلى الدالة غير المتزامنة
  3. تقوم الطريقة غير المتزامنة بالتحقق بشكل دوري من token.IsCancellationRequested أو تمريرها إلى واجهات برمجة التطبيقات القابلة للإلغاء
  4. اتصل بـ cts.Cancel() عند الحاجة إلى الإلغاء

▶ مثال

CSHARP
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");
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
Step 1
Step 2
Step 3
Step 4
Operation cancelled

8. IAsyncEnumerable<T> التدفقات غير المتزامنة

قدمت لغة C# 8 ميزة IAsyncEnumerable<T>، التي تدعم إنشاء واستهلاك تدفقات البيانات بشكل غير متزامن. استخدم await foreach للتكرار غير المتزامن للعناصر، وهو مناسب لسيناريوهات مثل القراءة المجمعة لقواعد البيانات واستلام استجابات الشبكة بشكل متدفق.

▶ مثال

CSHARP
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);
}
▶ جرّب الكود
TEXT 📖 للعرض فقط
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) لتجنب التقاط سياق التزامن، مما يمنع حدوث حالات التعطل ويحسن الأداء.

▶ مثال

CSHARP
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();
▶ جرّب الكود
TEXT 📖 للعرض فقط
Correct usage: Done
Library usage: Done

❓ أسئلة شائعة

س ما الفرق بين async void وasync Task؟
ج لا يمكن انتظار تنفيذ async void ولا يمكن التقاط الاستثناءات التي قد تحدث فيه؛ لذا يجب استخدامه فقط في معالجات الأحداث. أما async Task فهو النهج القياسي — حيث يمكن انتظار تنفيذه ويمكن معالجة الاستثناءات التي قد تحدث فيه.
س متى ينبغي عليّ استخدام Task.Run؟
ج فقط في عمليات الحوسبة التي تستهلك قدرًا كبيرًا من موارد وحدة المعالجة المركزية (CPU). أما عمليات الإدخال/الإخراج (I/O)، فينبغي استخدام واجهات برمجة التطبيقات (API) غير المتزامنة الأصلية (مثل ReadAsync) فيها، دون تغليفها باستخدام Task.Run.
س ما هي المشكلات التي قد تسببها .Result؟
ج في سياقات التزامن UI/ASP.NET، قد يؤدي ذلك إلى حدوث حالة تعطل متبادل — حيث ينتظر الخيط المستدعي اكتمال المهمة (Task)، بينما تنتظر المهمة (Task) تحرير سياق التزامن، مما يتسبب في حجب متبادل.
س متى ينبغي عليّ استخدام ConfigureAwait(false)؟
ج استخدمها في كود المكتبة لتجنب احتجاز سياق التزامن، مما يمنع حدوث حالات التعطل ويحسن الأداء. أما كود طبقة التطبيق في واجهة المستخدم/ASP.NET فلا يحتاج إليها عادةً.

📖 ملخص

📝 تمارين

  1. اكتب دالة غير متزامنة DownloadAllAsync تستخدم Task.WhenAll لتنزيل 3 مصادر بيانات محاكاة في وقت واحد، وتُرجع مصفوفة تحتوي على جميع النتائج
  2. اكتب دالة عد تنازلي غير متزامن تدعم CancellationToken، وتعرض الثواني المتبقية كل ثانية، وتطبع عبارة "تم إلغاء العد التنازلي" عند الإلغاء
  3. استخدم IAsyncEnumerable<int> لتوليد أول 20 عددًا من أعداد فيبوناتشي، ثم كرر العملية باستخدام await foreach، وقم بإخراج كل عدد على حدة
  4. حدد المشكلة في الشفرة التالية وقم بإصلاحها: async void Button_Click() { var data = GetDataAsync().Result; Console.WriteLine(data); }
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%