كيف تستدعي طريقة غير متزامنة من طريقة متزامنة في C#؟ الجوهر، لا حشو
\n\nتخيل: أنت جالس في المساء تكتب كودًا، وتنهي أداة وحدة تحكم قديمة بلغة C#. كل شيء يعمل بشكل متزامن، كالساعة. وفجأة! تحتاج إلى استدعاء API مصمم فقط عبر async Task. ألن تعيد كتابة البرنامج بأكمله؟ ألم مألوف، أليس كذلك؟
سؤال «How to call asynchronous method from synchronous method in C#» هو أحد أكثر الأسئلة شيوعًا على Stack Overflow. وليس بدون سبب. هناك، في التعليقات، أطنان من النصائح من «استخدم .Result ولا تقلق» إلى «هل أنت مجنون؟ ستحصل على deadlock!». دعنا نصنف الأمور: أي الأساليب تعمل حقًا، وأيها طريق إلى الجحيم.
\n\nلماذا لا يعمل ببساطة «await»؟
\n\nإذا حاولت كتابة:
\npublic void SyncMethod()\n{\n await DoSomethingAsync(); // خطأ CS4032\n}\nسيرفض المترجم ببساطة ترجمته. يمكن استخدام await فقط داخل طريقة مُعلَمة بالمُعدِّل async. أما async void فهي قصة أخرى سنناقشها أيضًا.
إذن، لدينا طريقة غير متزامنة:
\npublic 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الطريقة الأكثر شيوعًا، وربما الأكثر أناقة:
\npublic string CallSync()\n{\n return GetDataAsync().GetAwaiter().GetResult();\n}\n\nلماذا هذا رائع:
\n- \n
- لا يُنشئ استثناء
AggregateException(على عكس.Result). إذا حدث خطأ داخل الطريقة غير المتزامنة، ستحصل على نوعه الأصلي، وليس غلافًا. \n - الكود قابل للقراءة بشكل جيد، بدون try-catch زائدة. \n
لكن هناك فارق بسيط: إذا كانت طريقتك المتزامنة تُنفَّذ في سياق مع SynchronizationContext (مثل مؤشر ترابط واجهة المستخدم في WinForms أو WPF)، فإنك تخاطر بالحصول على deadlock — حجب متبادل للمؤشرات. المؤشر A ينتظر إكمال المهمة غير المتزامنة، والمهمة لا تستطيع الإكمال لأنها تنتظر تحرير المؤشر A. كارثة.
كيف تتجنب ذلك: استخدم .ConfigureAwait(false) داخل الطريقة غير المتزامنة نفسها. لكن هذا يغير API، وليس فقط كود الاستدعاء.
الطريقة 2: Task.Run(async () => await ...).GetAwaiter() — النقل إلى تجمع المؤشرات
\n\nإذا لم تستطع تغيير الطريقة غير المتزامنة، ولكنك تريد تجنب deadlock بشكل مضمون — انقل المهمة إلى تجمع المؤشرات:
\npublic string CallSyncSafe()\n{\n return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();\n}\n\nالسحر: Task.Run يُشغِّل المفوض على مؤشر ترابط من ThreadPool، الذي ليس لديه SynchronizationContext. deadlock مستحيل. حتى لو كانت الطريقة الأصلية تستخدم ConfigureAwait(true).
العيب: تبديل سياق إضافي (context switch). لمئات الاستدعاءات — غير مناسب. للاستدعاءات الفردية — مثالي.
\n\nالطريقة 3: .Result أو .Wait() — طريقة الجدات
\n\npublic string CallSyncOldSchool()\n{\n return GetDataAsync().Result; // أو .Wait()\n}\n\nما المشكلة:
\n- \n
- عند حدوث خطأ، يُلقي
AggregateException. للوصول إلى الخطأ الحقيقي، تحتاج إلى البحث فيInnerException. \n - نفس خطر deadlock في سياق واجهة المستخدم. \n
- الكود يبدو كحل مؤقت من عام 2012. \n
هذه الطريقة هي قطعة قماش حمراء لمراجعة الكود. إذا رأيت شيئًا كهذا في طلب دمج (pull request) — سأرفضه.
\n\nالطريقة 4: AsyncHelper أو RunSync مخصص
\n\nأحيانًا يكتب المطورون ذوو الخبرة أدوات مساعدة خاصة بهم. على سبيل المثال، هذه:
\n