استدعاء غير متزامن من كود متزامن في C#: معركة الأساليب

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

كيف تستدعي طريقة غير متزامنة من طريقة متزامنة في C#؟ الجوهر، لا حشو

\n\n

تخيل: أنت جالس في المساء تكتب كودًا، وتنهي أداة وحدة تحكم قديمة بلغة C#. كل شيء يعمل بشكل متزامن، كالساعة. وفجأة! تحتاج إلى استدعاء API مصمم فقط عبر async Task. ألن تعيد كتابة البرنامج بأكمله؟ ألم مألوف، أليس كذلك؟

\n\n

سؤال «How to call asynchronous method from synchronous method in C#» هو أحد أكثر الأسئلة شيوعًا على Stack Overflow. وليس بدون سبب. هناك، في التعليقات، أطنان من النصائح من «استخدم .Result ولا تقلق» إلى «هل أنت مجنون؟ ستحصل على deadlock!». دعنا نصنف الأمور: أي الأساليب تعمل حقًا، وأيها طريق إلى الجحيم.

\n\n

لماذا لا يعمل ببساطة «await»؟

\n\n

إذا حاولت كتابة:

\n
public void SyncMethod()\n{\n    await DoSomethingAsync(); // خطأ CS4032\n}
\n

سيرفض المترجم ببساطة ترجمته. يمكن استخدام await فقط داخل طريقة مُعلَمة بالمُعدِّل async. أما async void فهي قصة أخرى سنناقشها أيضًا.

\n\n

إذن، لدينا طريقة غير متزامنة:

\n
public async Task<string> GetDataAsync()\n{\n    using var httpClient = new HttpClient();\n    return await httpClient.GetStringAsync("https://api.example.com/data");\n}
\n\n

ولدينا طريقة متزامنة نريد استدعاءها منها. لنستعرض الخيارات.

\n\n

الطريقة 1: GetAwaiter().GetResult() — الأناقة الهاكر

\n\n

الطريقة الأكثر شيوعًا، وربما الأكثر أناقة:

\n
public string CallSync()\n{\n    return GetDataAsync().GetAwaiter().GetResult();\n}
\n\n

لماذا هذا رائع:

\n
    \n
  • لا يُنشئ استثناء AggregateException (على عكس .Result). إذا حدث خطأ داخل الطريقة غير المتزامنة، ستحصل على نوعه الأصلي، وليس غلافًا.
  • \n
  • الكود قابل للقراءة بشكل جيد، بدون try-catch زائدة.
  • \n
\n\n

لكن هناك فارق بسيط: إذا كانت طريقتك المتزامنة تُنفَّذ في سياق مع SynchronizationContext (مثل مؤشر ترابط واجهة المستخدم في WinForms أو WPF)، فإنك تخاطر بالحصول على deadlock — حجب متبادل للمؤشرات. المؤشر A ينتظر إكمال المهمة غير المتزامنة، والمهمة لا تستطيع الإكمال لأنها تنتظر تحرير المؤشر A. كارثة.

\n\n

كيف تتجنب ذلك: استخدم .ConfigureAwait(false) داخل الطريقة غير المتزامنة نفسها. لكن هذا يغير API، وليس فقط كود الاستدعاء.

\n\n

الطريقة 2: Task.Run(async () => await ...).GetAwaiter() — النقل إلى تجمع المؤشرات

\n\n

إذا لم تستطع تغيير الطريقة غير المتزامنة، ولكنك تريد تجنب deadlock بشكل مضمون — انقل المهمة إلى تجمع المؤشرات:

\n
public string CallSyncSafe()\n{\n    return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();\n}
\n\n

السحر: Task.Run يُشغِّل المفوض على مؤشر ترابط من ThreadPool، الذي ليس لديه SynchronizationContext. deadlock مستحيل. حتى لو كانت الطريقة الأصلية تستخدم ConfigureAwait(true).

\n\n

العيب: تبديل سياق إضافي (context switch). لمئات الاستدعاءات — غير مناسب. للاستدعاءات الفردية — مثالي.

\n\n

الطريقة 3: .Result أو .Wait() — طريقة الجدات

\n\n
public string CallSyncOldSchool()\n{\n    return GetDataAsync().Result; // أو .Wait()\n}
\n\n

ما المشكلة:

\n
    \n
  • عند حدوث خطأ، يُلقي AggregateException. للوصول إلى الخطأ الحقيقي، تحتاج إلى البحث في InnerException.
  • \n
  • نفس خطر deadlock في سياق واجهة المستخدم.
  • \n
  • الكود يبدو كحل مؤقت من عام 2012.
  • \n
\n\n

هذه الطريقة هي قطعة قماش حمراء لمراجعة الكود. إذا رأيت شيئًا كهذا في طلب دمج (pull request) — سأرفضه.

\n\n

الطريقة 4: AsyncHelper أو RunSync مخصص

\n\n

أحيانًا يكتب المطورون ذوو الخبرة أدوات مساعدة خاصة بهم. على سبيل المثال، هذه:

\n
                

المدونات

توصيات الكتب