تنظيم async وخيط الواجهة في WPF/WinForms في صفحة واحدة

· آخر تحديث: · · C#, async/await, .NET, WPF, WinForms, UI, الخيوط

سجل التعديلات (4 تحديثات، آخر تحديث 3 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240813)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
تُرجمت تعليقات توزيع خيط الواجهة في WPF/WinForms إلى العربية.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621335)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). تنظيم async وخيط الواجهة في WPF/WinForms في صفحة واحدة. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621335 https://comcomponent.com/ar/blog/2026/03/12/000-wpf-winforms-ui-thread-async-await-one-sheet/

DOI (أحدث نسخة)
10.5281/zenodo.21621335
DOI (هذه النسخة)
10.5281/zenodo.22279726

عند استخدام async / await في WPF / WinForms، أكثر ما يُحتار فيه إلى أيّ خيط نعود بعد await، ومتى يجوز لمس الواجهة. خصوصاً إذا اختلط Dispatcher وBeginInvoke وConfigureAwait(false) و.Result / .Wait()، يصعب رؤية سبب تجمّد الشاشة أو استثناء عبر الخيوط.

هذا المقال يتناول علاقة خيط الواجهة في WPF / WinForms بـ async / await فقط. محور الحكم الكلّيّ لـ async / await يتّصل بـ جدول حكم عمليّ لـ async/await في C# - Task.Run وConfigureAwait.

ما تفوح منه رائحة الدم حقّاً في العمل تقريباً هذا.

  • بعد await، لا يُعرَف أين تعمل التكملة
  • بعد إدخال Task.Run، لا يُعرَف هل يجوز لمس الواجهة
  • الحيرة أين يُوضَع ConfigureAwait(false)
  • تجمّد الشاشة بـ .Result / .Wait() / .GetAwaiter().GetResult()
  • اختلاط Dispatcher في WPF مع Invoke / BeginInvoke / InvokeAsync في WinForms في الرأس

WPF / WinForms كلاهما نموذج يتمحور حول خيط الواجهة. لذلك أكثر ما ينفع في ترتيب async / await ليس حديثاً فلسفيّاً عن «ما اللاتزامن»، بل توضيح ماذا تفعل الشيفرة بخيط الواجهة وحلقة الرسائل.

في هذا المقال، بافتراض تطبيقات WPF / WinForms على .NET 6 فما فوق أساساً، نتتبّع وجهة العودة بعد await، وDispatcher، وConfigureAwait(false)، وسبب التوقّف في .Result / .Wait()، بترتيب سهل الاستخدام في العمل.

لاحظ أنّ Control.InvokeAsync في WinForms من .NET 9 فصاعداً. في WinForms قبل ذلك يُستخدَم أساساً BeginInvoke / Invoke.

كذلك الشيفرة الواردة هنا منشورة على GitHub كمجموعة عيّنات يمكن بناؤها وتشغيلها: مكتبة لا تعتمد على الواجهة، وعيّنات WPF / WinForms، واختبارات وحدة تعيد إنتاج وجهة العودة بعد await وdeadlock.

wpf-winforms-ui-thread-async-await-one-sheet - komurasoft-blog-samples (GitHub)

المحتويات

  1. الخلاصة أوّلاً (في جملة)
  2. التنظيم أوّلاً في صفحة واحدة
    • 2.1. الصورة الكلّيّة
    • 2.2. جدول الحكم أوّلاً
  3. مصطلحات هذا المقال
    • 3.1. خيط الواجهة وحلقة الرسائل
    • 3.2. SynchronizationContext / Dispatcher / Invoke
  4. الأنماط النموذجيّة
    • 4.1. plain await في معالج حدث الواجهة
    • 4.2. Task.Run لحساب CPU الثقيل فقط
    • 4.3. ConfigureAwait(false) ليس «ضمان عدم العودة» بل «عدم فرض العودة»
    • 4.4. سبب التوقّف في .Result / .Wait() / .GetAwaiter().GetResult()
  5. متى يُستخدَم Dispatcher / Invoke
  6. أنماط مضادّة شائعة
  7. قائمة فحص عند المراجعة
  8. دليل اختيار تقريبيّ
  9. الخلاصة
  10. روابط مرجعيّة

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 26، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

1. الخلاصة أوّلاً (في جملة)

  • إذا استُخدم plain await في معالج حدث الواجهة في WPF / WinForms، فتكملة ما بعد await تعود أساساً إلى خيط الواجهة
  • Task.Run لإخراج حساب CPU من خيط الواجهة، وليس أداة للفّ انتظار I/O
  • حتّى مع await Task.Run(...) داخل معالج الواجهة، إذا كان ذلك await هو plain await فالتكملة تعود عادة إلى خيط الواجهة
  • ConfigureAwait(false) يعني عدم فرض العودة إلى سياق الواجهة الملتقَط في ذلك await. لمس الواجهة مباشرة في التكملة بعده خطر
  • .Result / .Wait() / .GetAwaiter().GetResult() يسدّ خيط الواجهة. إذا احتاج استكمال await العودة إلى الواجهة، يتوقّف الأمر بسهولة
  • للإعادة الصريحة إلى الواجهة في WPF: Dispatcher.InvokeAsync
  • للإعادة الصريحة إلى الواجهة في WinForms: سابقاً BeginInvoke، ومن .NET 9 فصاعداً InvokeAsync ينسجم مع تدفّق async
  • السياسة أوّلاً: الطبقة الخارجيّة للواجهة plain await، والمكتبة العامّة تنظر في ConfigureAwait(false)، والإعادة إلى الواجهة تُصرَّح بها في المواضع اللازمة فقط

باختصار، في WPF / WinForms

  1. على أيّ خيط نعمل الآن
  2. إلى أين تعود تكملة await
  3. من يحمل مسؤوليّة الإعادة إلى الواجهة

ضبط هذه الثلاث يحسّن الرؤية كثيراً.

ثلاثة أسئلة تحسّن الرؤيةيبيّن أنّ ضبط ثلاث: على أيّ خيط نعمل الآن، وإلى أين تعود تكملة await، ومن يحمل مسؤوليّة الإعادة إلى الواجهة، يحسّن رؤية الشيفرة اللاتزامنيّة في WPF وWinForms.على أيّ خيط نعمل الآنرؤية شيفرة الواجهة اللاتزامنيّةإلى أين تعود تكملة awaitمن يحمل مسؤوليّة الإعادة إلى الواجهة

الشكل 1: ثلاثة أسئلة يُرجَع إليها عند الحيرة. الخيط ووجهة العودة ومسؤوليّة الإعادة تُفكَّر منفصلة.

2. التنظيم أوّلاً في صفحة واحدة

2.1. الصورة الكلّيّة

الإمساك بالصورة الكلّيّة بهذا الرسم أسرع أوّلاً.

معالج حدث الواجهة(WPF / WinForms)plain awaitI/O APIإمساك UI SynchronizationContextالاستئناف بعد await على خيط الواجهةيمكن كتابة تحديث الواجهة كما هوawait Task.Run(...)معالجة CPU ثقيلةجسم الحساب على ThreadPoolالاستئناف بعد await على خيط الواجهةawait SomeAsync().ConfigureAwait(false)عدم فرض العودة إلى الواجهةالتكملة على أيّ خيطتحديث الواجهة مباشرة خطريلزم Dispatcher / InvokeSomeAsync().Result / Wait()GetAwaiter().GetResult()حجب خيط الواجهةالاستكمال لا يستطيع العودة إلى الواجهةتعليق / deadlock / تجمّد على الأقلّ

الشكل 2: الصورة الكلّيّة لأربعة أنماط من معالج الواجهة. plain await وTask.Run يعودان إلى الواجهة، وConfigureAwait(false) لا يفرض العودة، و.Result / .Wait() يسدّ خيط الواجهة.

ما يُشاهَد في العمل تقريباً هذه الأنماط الأربعة.

  1. plain await في معالج حدث الواجهة
  2. إفلات CPU بـ Task.Run في معالج حدث الواجهة
  3. إخراج وجهة العودة بـ ConfigureAwait(false)
  4. سدّ خيط الواجهة بـ .Result / .Wait()

2.2. جدول الحكم أوّلاً

الوضع أين يعمل أثناء الانتظار تكملة ما بعد await هل يجوز لمس الواجهة مباشرة الاختيار أوّلاً
await SomeIoAsync() في معالج الواجهة انتظار اكتمال I/O. خيط الواجهة نفسه يمكنه العودة إلى حلقة الرسائل أساساً خيط الواجهة نعم plain await
await Task.Run(...) في معالج الواجهة CPU الثقيل على ThreadPool أساساً خيط الواجهة نعم Task.Run لـ CPU فقط
await x.ConfigureAwait(false) في معالج الواجهة لا تُثبَّت وجهة العودة على الواجهة أيّ خيط لا يُتجنَّب أساساً في شيفرة الواجهة
x.Result / x.Wait() على خيط الواجهة خيط الواجهة يُسدّ بالانتظار الاستكمال أصلاً يصعب دورانه لا لا يُستخدَم
إرادة تحديث الواجهة بعد خيط خلفيّة أو ConfigureAwait(false) يعمل على خيط غير الواجهة ليس الواجهة كما هو لا Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync
كتابة مكتبة عامّة لا تعتمد على الواجهة لا تعتمد على ظروف المستدعي لا تفرض الإعادة إلى الواجهة تصميم لا يلمسها النظر في ConfigureAwait(false)
إرادة استدعاء async من منشئ أو خاصّيّة متزامنة خيط الواجهة يدخل الانتظار بسهولة مسار التشغيل يسهل توقّفه لا الإفلات إلى Loaded / Shown / InitializeAsync

المهمّ في هذا الجدول أنّ plain await في شيفرة الواجهة حليف بالأحرى. العدو ليس await نفسه، بل سدّ خيط الواجهة تزامنيّاً.

الاختيار نفسه مُجمَّع في هذا الجدول. الفصول التالية تقسيم: لماذا صار الجدول هكذا (الفصلان 3 و4)، اختيار أداة الإعادة إلى الواجهة (الفصل 5)، كيف تُوجَد عند المراجعة (الفصلان 6 و7).

العدو ليس await بل الحجب التزامنيّيبيّن الترتيب أنّ plain await في شيفرة الواجهة حليف بالأحرى، وأنّ العدو الحقيقيّ سدّ خيط الواجهة تزامنيّاً.plain awaitحليف بالأحرى في شيفرة الواجهةسدّ خيط الواجهة تزامنيّاًمصدر التجمّد وdeadlock

الشكل 3: الترتيب في لبّ جدول الحكم. العدو ليس await نفسه، بل سدّ خيط الواجهة تزامنيّاً.

3. مصطلحات هذا المقال

3.1. خيط الواجهة وحلقة الرسائل

واجهة WPF / WinForms أساساً شكل خيط واجهة واحد يدير الإدخال والرسم ومعالجة الأحداث.

دور خيط الواجهة هذا تقريباً هكذا.

  • معالجة رسائل مثل ضغط الزرّ وإدخال المفاتيح وإعادة الرسم
  • الخيط الوحيد الذي يمكنه لمس عناصر التحكّم وكائنات الواجهة بأمان
  • إذا حُشيت فيه معالجة أكثر ممّا يلزم، يتوقّف تحديث الشاشة واستجابة الإدخال

اللبّ هنا أنّ عمل خيط الواجهة «الدوران بسرعة». إذا حُجب هنا طويلاً، يتوقّف الفأرة ولوحة المفاتيح وإعادة الرسم، فيبدو للمستخدم «تجمّد».

وضع هذه الصورة في الرأس كرسم يقلّل الاختلاط.

إدخال المستخدم / طلب إعادة الرسمحلقة رسائل خيط الواجهةتنفيذ معالج الحدثتحديث الشاشةمعالجة متزامنة طويلةحلقة الرسائل لا تدورالشاشة تبدو متجمّدة

الشكل 4: عمل خيط الواجهة دوران حلقة الرسائل بسرعة، وإذا توسّطت معالجة متزامنة طويلة تتوقّف الحلقة فتبدو الشاشة متجمّدة.

3.2. SynchronizationContext / Dispatcher / Invoke

الكلمات الشائعة هنا، مقسومة للعمل، هكذا.

الكلمة المعنى هنا
خيط الواجهة الخيط الذي أنشأ كائن الواجهة. أساساً هو وحده يلمس الواجهة بأمان
حلقة الرسائل آليّة يعالج بها خيط الواجهة الرسائل بالترتيب
SynchronizationContext تجريد لـ «إعادة المعالجة إلى موضع التنفيذ ذلك»
Dispatcher طابور خيط الواجهة في WPF
Invoke / BeginInvoke / InvokeAsync API لإلقاء المعالجة إلى خيط الواجهة

بتدقيق أكثر في كيف تُقرَّر وجهة الاستكمال: ما يمسكه await (ما يعادل ConfigureAwait(true) الافتراضيّ) أوّلاً هو SynchronizationContext.Current. فقط عندما يكون null ينظر إلى TaskScheduler.Current، وإذا لم يكن TaskScheduler.Default يعيد الاستكمال إلى ذلك TaskScheduler. إن لم يكن أيّ منهما، أي إذا كان SynchronizationContext.Current هو null وTaskScheduler.Current هو الافتراضيّ، يعمل الاستكمال على ThreadPool. على خيط الواجهة في WPF / WinForms الحالة الأولى، أي أنّ SynchronizationContext الواجهة قائم، لذا في العمل لا حرج في اعتبار أنّ SynchronizationContext الواجهة نافذ.

كيف تُقرَّر وجهة استكمال awaitيبيّن أنّ await الافتراضيّ يمسك أوّلاً SynchronizationContext.Current، وإذا كان null ينظر إلى TaskScheduler.Current، فإن لم يكن الافتراضيّ يعيد إلى ذلك TaskScheduler، وإلا يعمل الاستكمال على ThreadPool.يوجدnullليس الافتراضيّافتراضيّawait الافتراضيّهل يوجد SynchronizationContext؟الإعادة إلى ذلك السياقهل TaskScheduler افتراضيّ؟الإعادة إلى ذلك TaskSchedulerتنفيذ الاستكمال على ThreadPoolعلى خيط الواجهة سياق الواجهة

الشكل 5: كيف تُقرَّر وجهة الاستكمال. على خيط الواجهة SynchronizationContext الواجهة قائم، لذا تعود التكملة إلى الواجهة.

المقابلة حسب الإطار أوضح كجدول.

الإطار سياق جانب الواجهة API تمثيليّ للإعادة الصريحة إلى الواجهة
WPF DispatcherSynchronizationContext Dispatcher.InvokeAsync / Dispatcher.BeginInvoke / Dispatcher.Invoke
WinForms WindowsFormsSynchronizationContext Control.BeginInvoke / Control.Invoke / .NET 9+ Control.InvokeAsync

WPF يتمحور حول Dispatcher. WinForms يتمحور حول مقبض عنصر التحكّم وحلقة الرسائل، فيظهر BeginInvoke / Invoke.

في العمل، تذكّر علاقة التجريد بالواقع بهذا القدر يقلّل الاختلاط.

الشيفرة الحاليّةSynchronizationContextWPF: DispatcherSynchronizationContextWinForms: WindowsFormsSynchronizationContextDispatcher.InvokeAsync / BeginInvoke / InvokeControl.BeginInvoke / Invoke / InvokeAsync (.NET 9+)

الشكل 6: واقع تجريد SynchronizationContext يتّصل في WPF بعائلة Dispatcher، وفي WinForms بعائلة Invoke في Control.

4. الأنماط النموذجيّة

4.1. plain await في معالج حدث الواجهة

الشكل الأصرح.

private async void LoadButton_Click(object sender, RoutedEventArgs e)
{
    LoadButton.IsEnabled = false;
    StatusText.Text = "جارٍ التحميل...";

    try
    {
        string text = await File.ReadAllTextAsync(FilePathTextBox.Text);
        PreviewTextBox.Text = text;
        StatusText.Text = "اكتمل";
    }
    catch (Exception ex)
    {
        StatusText.Text = ex.Message;
    }
    finally
    {
        LoadButton.IsEnabled = true;
    }
}

في هذه الشيفرة يبدأ LoadButton_Click على خيط الواجهة. وawait File.ReadAllTextAsync(...) هو plain await، لذا عادة يمسك سياق الواجهة في تلك اللحظة.

لذلك يصير الشكل:

  • أثناء انتظار I/O الملفّ لا يُحتلّ خيط الواجهة
  • تكملة ما بعد اكتمال القراءة تعود أساساً إلى خيط الواجهة
  • يمكن كتابة PreviewTextBox.Text = text; كما هو

هنا لا يلزم Dispatcher زائد. إذا استُخدم plain await فقط داخل معالج الواجهة، يمكن عادة لمس الواجهة كما هو.

كون هذا المعالج async void لأنّ توقيع معالج حدث الواجهة يطلب void، وهذا async void مسموح استثناءً. لذلك سبب وضع try / catch داخله واضح. في async Task يركب الاستثناء على Task القيمة المعادة، فيستلمه المستدعي عند await. ليس لـ async void ذلك Task، لذا الاستثناء الذي خرج يُعاد رميه إلى SynchronizationContext عند بدء ذلك المعالج، أي خيط الواجهة. استثناء غير معالَج على خيط الواجهة يخرج في WPF إلى Application.DispatcherUnhandledException، وفي WinForms إلى Application.ThreadException، وإذا لم يُعالَج هناك يسقط التطبيق.

أي أنّ الأساس في معالج async void الإمساك داخل المعالج، وبشكل المثال أعلاه «تحويل الفشل إلى عرض حالة، وإعادة الزرّ في finally» يُغلَق كواجهة بشكل طبيعيّ. شبكة التطبيق كلّه (DispatcherUnhandledException وغيرها) تُوضَع كشبكة أخيرة فقط.

وجهة استثناء async voidيبيّن أنّ استثناء async Task يركب على Task القيمة المعادة فيستلمه المستدعي بـ await، أمّا في async void فيُعاد رمي الاستثناء الذي خرج إلى SynchronizationContext عند البدء أي خيط الواجهة، وإذا لم يُعالَج في الشبكة الكلّيّة يسقط التطبيق.async Taskasync voidاستثناء خرج من المعالجasync Task أم async void؟يركب على Task القيمة المعادةالمستدعي الذي استخدم await يستلمهيُعاد رميه إلى خيط الواجهةيخرج إلى الشبكة الكلّيّةإن لم يُعالَج يسقط التطبيق

الشكل 7: ليس لـ async void Task يحمل الاستثناء، لذا الأساس الإمساك داخل المعالج.

في WinForms النظرة نفسها. ما دام plain await داخل معالج Click، فالتكملة تعود أساساً إلى جانب الواجهة.

كرسم، التدفّق هكذا.

UI SynchronizationContextI/O لاتزامنيّخيط الواجهةUI SynchronizationContextI/O لاتزامنيّخيط الواجهةأثناء الانتظار العودة إلى حلقة الرسائلبدء معالج Clickawait لـ ReadAllTextAsyncحجز إعادة التكملة إلى الواجهةاكتمال I/Oاستئناف التكملة على خيط الواجهةتحديث TextBox / Label

الشكل 8: أثناء انتظار plain await يعود خيط الواجهة إلى حلقة الرسائل، وتكملة ما بعد اكتمال I/O تُستأنف على خيط الواجهة.

4.2. Task.Run لحساب CPU الثقيل فقط

ما ينفع فيه Task.Run هو عندما يُراد إخراج حساب CPU ثقيل من خيط الواجهة.

private async void HashButton_Click(object sender, RoutedEventArgs e)
{
    HashButton.IsEnabled = false;
    ResultText.Text = "جارٍ الحساب...";

    try
    {
        byte[] data = await File.ReadAllBytesAsync(InputPathTextBox.Text);

        string hash = await Task.Run(() =>
        {
            using SHA256 sha256 = SHA256.Create();
            byte[] digest = sha256.ComputeHash(data);
            return Convert.ToHexString(digest);
        });

        ResultText.Text = hash;
    }
    catch (Exception ex)
    {
        ResultText.Text = ex.Message;
    }
    finally
    {
        HashButton.IsEnabled = true;
    }
}

ما يحدث في هذه الشيفرة تقريباً هكذا.

  1. يبدأ معالج الحدث على خيط الواجهة
  2. انتظار I/O لـ File.ReadAllBytesAsync يُمرَّر لاتزامنيّاً
  3. حساب التجزئة الثقيل فقط يُخرَج إلى ThreadPool بـ Task.Run
  4. تكملة await Task.Run(...) plain await فتعود إلى خيط الواجهة
  5. يمكن كتابة ResultText.Text = hash; كما هو

أي أنّ داخل Task.Run فقط هو الخيط الآخر. ليس أنّه يذهب بشكل دائم إلى «موضع لم يعد الواجهة» حتّى بعد await.

النظر إلى هذا في صفحة واحدة يقلّل سوء الفهم.

ThreadPoolI/O لاتزامنيّخيط الواجهةThreadPoolI/O لاتزامنيّخيط الواجهةتكملة await Task.Run(...) تُستأنف على الواجهةawait لـ ReadAllBytesAsyncplain await فيُستأنف على الواجهةإلقاء معالجة CPU الثقيلة بـ Task.Runإعادة نتيجة الحسابانعكاس النتيجة على الشاشة

الشكل 9: داخل Task.Run فقط يعمل على ThreadPool، وتكملة await تعود إلى خيط الواجهة لذا يُكتَب انعكاس الشاشة كما هو.

هنا تنبيهان.

  • لا تُلفّ انتظار I/O بـ Task.Run
  • اعتبر Task.Run ليس «جعل الأمر لاتزامنيّاً» بل صنع «وجهة إفلات CPU»

كتابة مثل Task.Run(async () => await File.ReadAllTextAsync(...)) تعيد إلقاء انتظار I/O على ThreadPool بلا جدوى، فلا نفع يُذكَر.

موضع استخدام Task.Runيبيّن أنّ دور Task.Run إفلات حساب CPU الثقيل إلى ThreadPool، وأنّ لفّ انتظار I/O يعيد إلقاء الانتظار على ThreadPool بلا جدوى.حساب CPU ثقيلانتظار I/Oما المراد إفلاته؟الإفلات بـ Task.Runلا يُلفّ بـ Task.Runإعادة إلقاء الانتظار بلا جدوى

الشكل 10: Task.Run ليس أداة «جعل الأمر لاتزامنيّاً»، بل أداة تصنع وجهة إفلات CPU.

4.3. ConfigureAwait(false) ليس «ضمان عدم العودة» بل «عدم فرض العودة»

هذا أكثر موضع يُساء فهمه.

أوّلاً، ما يناسب ConfigureAwait(false) هو شيفرة مكتبة عامّة لا تعتمد على الواجهة أو نموذج تطبيق معيّن.

public sealed class DocumentRepository
{
    public async Task<string> LoadNormalizedTextAsync(string path, CancellationToken cancellationToken)
    {
        string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
        return text.Replace("\r\n", "\n", StringComparison.Ordinal);
    }
}

هذه الدالّة لا تلمس الواجهة. شكل يُستخدَم في WPF وفي WinForms وفي ASP.NET Core وفي worker. في شيفرة كهذه وضع ConfigureAwait(false) طبيعيّ.

واستدعاء جانب الواجهة يكفي أن يكون plain await.

private readonly DocumentRepository _repository = new();

private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
    OpenButton.IsEnabled = false;
    StatusText.Text = "جارٍ التحميل...";

    try
    {
        string text = await _repository.LoadNormalizedTextAsync(
            PathTextBox.Text,
            CancellationToken.None);

        PreviewTextBox.Text = text;
        StatusText.Text = "اكتمل";
    }
    catch (Exception ex)
    {
        StatusText.Text = ex.Message;
    }
    finally
    {
        OpenButton.IsEnabled = true;
    }
}

المهمّ هنا أنّ ConfigureAwait(false) داخل المكتبة لا يفرض false قسراً حتّى await المستدعي.

أي يمكن الفصل:

  • داخل المكتبة لا يعود إلى الواجهة
  • إذا استخدم معالج الواجهة plain await لذلك، فتكملة المستدعي تعود إلى الواجهة
فصل المكتبة عن الواجهةيبيّن الفصل أنّ await داخل المكتبة العامّة لا يطلب العودة إلى سياق الواجهة بـ ConfigureAwait(false)، وأنّ معالج الواجهة إذا استخدم plain await لذلك فتكملة المستدعي تعود إلى الواجهة.await داخل المكتبةConfigureAwait (false)لا يطلب العودة إلى الواجهةplain await في معالج الواجهةتكملة المستدعي تعود إلى الواجهةتعيين الداخل لا يمتدّ إلى الخارج

الشكل 11: ConfigureAwait(false) داخل المكتبة لا يفرض false قسراً حتّى await المستدعي.

بالمقابل، الكتابة هكذا في معالج الواجهة نفسه خطر.

private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
    string text = await _repository.LoadNormalizedTextAsync(
        PathTextBox.Text,
        CancellationToken.None).ConfigureAwait(false);

    PreviewTextBox.Text = text;
}

في هذه الحالة، تكملة ذلك await في OpenButton_Click لا تفرض العودة إلى الواجهة. لذلك قد يصير PreviewTextBox.Text = text; وصولاً عبر الخيوط.

نقطة أخرى مهمّة بهدوء. حتّى مع ConfigureAwait(false) لا ينتقل الأمر حتماً إلى ThreadPool، وإذا اكتمل ذلك await فوراً دون انتظار قد تسيل التكملة على الخيط الحاليّ كما هي. قراءتها كـ «يذهب حتماً إلى خيط آخر» أو «من هنا فصاعداً لم يعد الواجهة دائماً» مصدر حوادث، والمعنى فقط عدم فرض إعادة استكمال ذلك await إلى سياق الواجهة الأصليّ. هذا فقط.

كرسم هكذا.

لانعمawait في معالج الواجهةهل يُوضَع ConfigureAwait(false)؟التكملة أساساً خيط الواجهةيسهل تحديث الواجهة كما هوالتكملة لا تُثبَّت على الواجهةقد تُستأنف على أيّ خيطتحديث الواجهة يلزم Dispatcher / Invoke

الشكل 12: ما يتغيّر بوجود ConfigureAwait(false) أو غيابه فقط «هل تُفرَض إعادة التكملة إلى الواجهة».

4.4. سبب التوقّف في .Result / .Wait() / .GetAwaiter().GetResult()

هذا أكثر حادث يُشاهَد.

private void LoadButton_Click(object sender, RoutedEventArgs e)
{
    string text = LoadTextAsync().Result;
    PreviewTextBox.Text = text;
}

private async Task<string> LoadTextAsync()
{
    string text = await File.ReadAllTextAsync(FilePathTextBox.Text);
    return text.ToUpperInvariant();
}

للوهلة الأولى يبدو مجرّد أخذ النتيجة تزامنيّاً، لكنّه خطر على خيط الواجهة.

التدفّق كرسم هكذا.

UI SynchronizationContextI/O لاتزامنيّخيط الواجهةUI SynchronizationContextI/O لاتزامنيّخيط الواجهةلكنّ الواجهة مسدودة بـ .Resultالاستكمال لا يدور فلا يكتملبدء LoadButton_Clickاستدعاء LoadTextAsync()إعادة Task غير مكتملالانتظار والحجب بـ .Resultاكتمال I/O، إرادة إعادة الاستكمال إلى الواجهةإرادة تنفيذ التكملة

الشكل 13: تدفّق لا يستطيع فيه الاستكمال العودة إلى خيط الواجهة المسدود بـ .Result، فلا يكتمل Task أبداً.

ما يحدث بكلمات هكذا.

  1. خيط الواجهة يستدعي LoadTextAsync()
  2. await داخل LoadTextAsync() يمسك سياق الواجهة
  3. خيط الواجهة ينتظر بـ .Result
  4. يكتمل I/O
  5. تكملة LoadTextAsync() تريد العودة إلى خيط الواجهة
  6. لكنّ خيط الواجهة مسدود بـ .Result
  7. لا تعمل التكملة فلا تكتمل LoadTextAsync()
  8. .Result لا ينتهي

أي أنّ الواجهة تقول «أنتظر حتّى تنتهي»، وجانب اللاتزامن يقول «أنتهي إذا عدت إلى الواجهة»، فينتظر كلّ منهما الآخر. شعور كريه حقّاً.

تركيب انتظار متبادليبيّن أنّ خيط الواجهة ينتظر اكتمال المعالجة اللاتزامنيّة بـ .Result، واستكمال الجانب اللاتزامنيّ ينتظر فراغ خيط الواجهة، فينتظر كلّ منهما الآخر فلا يتقدّم الأمر.خيط الواجهة ينتظر بـ .Resultيلزم اكتمال الجانب اللاتزامنيّالاستكمال يلزم العودة إلى الواجهةيلزم فراغ خيط الواجهةانتظار متبادل فلا يتقدّم الأمر

الشكل 14: يصطدم «أنتظر حتّى تنتهي» مع «أنتهي إذا عدت إلى الواجهة»، فينتظر كلّ منهما الآخر.

سوء الفهم الشائع هنا الاعتقاد أنّ GetAwaiter().GetResult() آمن. لكنّ جوهر سدّ خيط الواجهة واحد. ما يختلف أساساً تغليف الاستثناء.

لذلك في الواجهة أأمن معاملة هذه الثلاث الرائحة نفسها.

  • .Result
  • .Wait()
  • .GetAwaiter().GetResult()

لاحظ أنّ Task الذي تعيده Dispatcher.InvokeAsync(...) من نوع DispatcherOperation في WPF، إذا انتُظر بـ Task.Wait() من خيط الواجهة، خطر للسبب نفسه. InvokeAsync يكدّس المفوَّض الممرَّر في طابور Dispatcher فقط، والتنفيذ الفعليّ عندما يدير خيط الواجهة ذلك الطابور. إذا توقّف خيط الواجهة بـ Wait() لا يدور الطابور، فلا يكتمل ذلك Task أبداً. الكلام نفسه في جانب DispatcherOperation أيضاً، وDispatcherOperation.Wait() منصوص على أنّ انتظار عمليّة قيد التنفيذ على الخيط نفسه يرمي InvalidOperationException. أي أنّ مسار الانتظار بالحجب نفسه غير مفترَض. في سياق الواجهة، اتّجاه «انتظار ما أُلقي تزامنيّاً» نفسه يسهل توقّفه. شرح أوضح لشكل التوقّف في Await, and UI, and deadlocks! Oh my!.

خطر انتظار InvokeAsync تزامنيّاًيبيّن أنّ Dispatcher.InvokeAsync يكدّس المفوَّض في الطابور فقط، والتنفيذ عندما يدير خيط الواجهة ذلك الطابور، فإذا توقّف خيط الواجهة بـ Wait لا يدور الطابور فلا يكتمل Task أبداً.التكديس في الطابور بـ InvokeAsyncالتنفيذ عندما يدير خيط الواجهة الطابورتوقّف خيط الواجهة بـ Waitالطابور لا يدورTask لا يكتمل أبداً

الشكل 15: إذا انتظر خيط الواجهة نفسه ما ألقاه تزامنيّاً، لا يدور الطابور وهو موضع التنفيذ فلا ينتهي الأمر أبداً.

«هل يحدث deadlock حتماً؟» ليس بالضرورة كذلك. إذا صادف أنّ الشيفرة لا تعيد الاستكمال إلى الواجهة، قد تجمّد الواجهة فقط دون deadlock. لكنّ ذلك أيضاً مؤلم بما يكفي، لذا في الواجهة الأفضل أساساً عدم فعله.

5. متى يُستخدَم Dispatcher / Invoke

بناء على ما سبق، في معالج واجهة بـ plain await لا يلزم عادة Dispatcher / Invoke صريح.

يصير لازماً مثلاً في أوقات كهذه.

  • إرادة لمس الواجهة في تكملة ConfigureAwait(false)
  • تكوين لا يعود إلى الواجهة داخل Task.Run أو خارجه أيضاً
  • إشعار يأتي من موضع ليس خيط الواجهة من الأصل، مثل استقبال مقبس أو مؤقّت أو callback حدث
  • طبقة فصلت الواجهة عن غير الواجهة عمداً، ويُراد التصريح بتحديث الواجهة الأخير فقط

في WPF التمثيليّ Dispatcher.InvokeAsync.

private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
    string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);

    await Dispatcher.InvokeAsync(() =>
    {
        PreviewTextBox.Text = text;
        StatusText.Text = "اكتمل";
    });
}

في WinForms من .NET 9 فصاعداً، InvokeAsync ينسجم بصراحة مع تدفّق async.

private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
    string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);

    await previewTextBox.InvokeAsync(() =>
    {
        previewTextBox.Text = text;
        statusLabel.Text = "اكتمل";
    });
}

في النمط السابق لـ WinForms يُستخدَم BeginInvoke. Invoke إرسال متزامن ينتظر المستدعي. BeginInvoke ينشر ويعود فوراً. في تدفّق async، جانب عدم الحجب أساساً أنسب للانسجام.

الفرق بين Invoke وBeginInvokeيبيّن أنّ Invoke إرسال متزامن ينتظر المستدعي، وأنّ BeginInvoke ينشر ويعود فوراً، لذا في تدفّق async جانب عدم الحجب أنسب للانسجام.Invoke (إرسال متزامن)ينتظر المستدعيBeginInvoke (نشر)يعود فوراًينسجم مع تدفّق async

الشكل 16: حتّى في «الإلقاء إلى الواجهة» نفسه، تختلف طبيعة Invoke الذي ينتظر عن BeginInvoke الذي يعود فوراً.

لكنّ ما يعيده Control.BeginInvoke هو IAsyncResult، لذا لا يمكن await كما هو. إذا أُريد حمله على تدفّق async في بيئة بلا Control.InvokeAsync (.NET Framework 4.8 و.NET 6 / 8 وغيرها)، فاللفّ بـ TaskCompletionSource وجعله Task صريح.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

public static class ControlUiExtensions
{
    // نستخدم النسخة generic من TaskCompletionSource حتى تعمل كما هي على
    // .NET Framework 4.8. من .NET 5 فصاعدا يمكن كتابتها بالنسخة غير generic.
    //
    // cancellationToken ليس اختياريّا عن عمد. إذا صار الـ Control disposed بعد أن
    // يقبل BeginInvoke الـ delegate, يُطرَح الـ delegate المنشور دون أن يُنفَّذ,
    // ولا يصل إلى TaskCompletionSource لا نتيجة ولا استثناء. إن لم يُوفَّر منفذ
    // للقطع, يبقى من ينتظر بـ await معلَّقا إلى الأبد
    public static Task InvokeOnUiAsync(
        this Control control, Action action, CancellationToken cancellationToken)
    {
        if (control is null)
        {
            throw new ArgumentNullException(nameof(control));
        }

        if (action is null)
        {
            throw new ArgumentNullException(nameof(action));
        }

        if (!control.IsHandleCreated)
        {
            throw new InvalidOperationException("مقبض النافذة لم يُنشأ بعد.");
        }

        if (!control.InvokeRequired)
        {
            action();
            return Task.CompletedTask;
        }

        // نصرّح بتشغيل الاستكمال لاتزامنيّا, حتى لا تركض تكملة جانب await
        // كما هي على خيط الواجهة.
        var tcs = new TaskCompletionSource<bool>(
            TaskCreationOptions.RunContinuationsAsynchronously);

        // الإلغاء والتنفيذ يتنافسان على «حقّ لمرّة واحدة» نفسه.
        // وحده من كتب 1 أوّلا عبر Interlocked.Exchange يتقدّم.
        // أسلوب النظر إلى علم ثم استدعاء action() يترك مسارا إذا أُلغي فور النظر:
        // المستدعي تلقّى الإلغاء وبدأ العملية التالية, ثم يعيد الـ delegate
        // القديم كتابة الشاشة لاحقا
        int claimed = 0;   // 0 = غير محسوم / 1 = أحد الجانبين أخذه

        // عند الإلغاء يُغلَق الـ Task حتى إن لم يُنفَّذ الـ delegate.
        // يُزال التسجيل حتما عند اكتمال الـ Task (وإلا يظلّ ممسكا tcs طوال حياة
        // الـ token). CancellationTokenRegistration.Dispose آمن للخيوط, فيجوز
        // استدعاؤه من أيّ خيط
        CancellationTokenRegistration registration = cancellationToken.Register(() =>
        {
            if (Interlocked.Exchange(ref claimed, 1) == 0)
            {
                tcs.TrySetCanceled(cancellationToken);
            }
        });

        tcs.Task.ContinueWith(
            _ => registration.Dispose(),
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);

        try
        {
            control.BeginInvoke(new Action(() =>
            {
                // قد يحدث الإلغاء بين النشر ولحظة تحريك خيط الواجهة.
                // إن تعذّر أخذ الحقّ هنا, فجانب الإلغاء أخذه أوّلا, لذا نعود
                // دون لمس الشاشة إطلاقا
                if (Interlocked.Exchange(ref claimed, 1) != 0)
                {
                    return;
                }

                try
                {
                    action();
                    tcs.TrySetResult(true);
                }
                catch (Exception ex)
                {
                    tcs.TrySetException(ex);
                }
            }));
        }
        catch (Exception ex)
        {
            // BeginInvoke نفسه قد يرمي أيضا (مثلا لم يعد هناك handle).
            // إن لم يُغلَق الـ Task هنا, يبقى الانتظار معلَّقا أيضا.
            // الـ delegate لن يركض, لذا نأخذ الحقّ هنا أيضا ثم نغلق
            if (Interlocked.Exchange(ref claimed, 1) == 0)
            {
                tcs.TrySetException(ex);
            }
        }

        return tcs.Task;
    }
}

جانب الاستدعاء يصير شكلاً شبه مطابق لمثال InvokeAsync. اربط الرمز بعمر النموذج.

// حقل في النموذج. يُلغى عند الإغلاق
private readonly CancellationTokenSource _formClosing = new();

protected override void OnFormClosed(FormClosedEventArgs e)
{
    // حتى إن طُرح delegate منشور دون تنفيذ,
    // يتيح هذا إغلاق جانب await هنا
    _formClosing.Cancel();
    base.OnFormClosed(e);
}

private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
    using var linked = CancellationTokenSource.CreateLinkedTokenSource(
        cancellationToken, _formClosing.Token);

    string text = await File.ReadAllTextAsync(path, linked.Token).ConfigureAwait(false);

    await previewTextBox.InvokeOnUiAsync(() =>
    {
        previewTextBox.Text = text;
        statusLabel.Text = "اكتمل";
    }, linked.Token);
}

بهذا الشكل يُستلَم أيضاً الاستثناء الذي حدث في جانب الواجهة في try / catch عند موضع await. أربع نقاط تُضبَط.

  • الإلغاء والتنفيذ لا يكفيان بـ «انظر إلى العلم ثمّ اعمل». فور تأكيد «هل أُلغي؟»، قد يجري الإلغاء قبل استدعاء action(). في هذه اللحظة يصير tcs ملغى، والمستدعي الذي كان ينتظر بـ await يتقدّم ويبدأ العمليّة التالية. بعد ذلك يعمل المفوَّض القديم الذي ما زال في الطابور فيعيد كتابة الشاشة ── شكل انكسار صعب إعادة الإنتاج: العرض الجديد يُكتَب فوقه عرض قديم. لذلك تتنازع الشيفرة أعلاه على «حقّ مرّة واحدة» بـ Interlocked.Exchange، والجانب الذي لم يأخذه يعود دون فعل شيء
  • BeginInvoke قبل إنشاء المقبض (قبل Load) أو بعد إغلاق النموذج يصير استثناء. انتبه إلى عمر جانب الاستدعاء
  • إذا أُتلف عنصر التحكّم بعد النشر، قد يُرمى المفوَّض دون تنفيذ. عندئذ لا يدخل في TaskCompletionSource نتيجة ولا استثناء، فينتظر جانب await أبداً. مرّر حتماً رمزاً مربوطاً بإنهاء النموذج كما في المثال أعلاه. النتيجة المطوية تعلو كـ OperationCanceledException
  • File.ReadAllTextAsync API من .NET Core 2.0 فصاعداً. للشكل نفسه على .NET Framework 4.8 استبدله بـ StreamReader.ReadToEndAsync أو نحوه
تنازع حقّ الإلغاء والتنفيذيبيّن منع سباق يعيد فيه المفوَّض القديم كتابة الشاشة بتنازع جانب الإلغاء وجانب التنفيذ على حقّ مرّة واحدة بـ Interlocked.Exchange، فيتقدّم من أخذه أوّلاً فقط، ويعود من لم يأخذه دون فعل شيء.حقّ مرّة واحدةجانب الإلغاء يأخذه أوّلاًجانب التنفيذ يأخذه أوّلاًطيّ Task كملغىالمفوَّض القديم يعود دون فعل شيءتنفيذ action وإدخال النتيجة

الشكل 17: ليس النظر إلى العلم ثمّ العمل، بل التنازع على حقّ مرّة واحدة وعودة من لم يأخذه.

كتمييز يكفي هذا القدر.

المراد WPF WinForms
الدخول إلى الواجهة تزامنيّاً Dispatcher.Invoke Control.Invoke
الإلقاء إلى الواجهة لاتزامنيّاً Dispatcher.InvokeAsync / Dispatcher.BeginInvoke Control.BeginInvoke / .NET 9+ Control.InvokeAsync
الانسجام الصريح مع async / await Dispatcher.InvokeAsync .NET 9+ Control.InvokeAsync، وقبل ذلك BeginInvoke

كإحساس في العمل،

  • لا يلزم إذا استُخدم plain await فقط في معالج الواجهة
  • يُستخدَم عندما يُراد لمس الواجهة من موضع غير الواجهة
  • لا تُكثَّر Invoke المتزامن أكثر ممّا يلزم داخل تدفّق async

بهذا تقلّ الحوادث كثيراً.

عند الحيرة يكفي رسم حكم بهذا القدر.

نعملالانعمنعمموضع كتابة هذه التكملة خيط الواجهة؟نعم؟تحديث الواجهة بـ plain await كما هو جائزهل يُراد لمس الواجهة؟متابعة المعالجة كما هيWPF: Dispatcher.InvokeAsyncWinForms: BeginInvoke / InvokeAsync

الشكل 18: يُحكَم بكون موضع كتابة التكملة خيط الواجهة أم لا هل يكفي plain await أم تلزم عائلة Dispatcher / Invoke.

6. أنماط مضادّة شائعة

النمط المضادّ ما الذي يؤلم الاستبدال أوّلاً
LoadAsync().Result في معالج الواجهة يسدّ خيط الواجهة. يسهل deadlock await LoadAsync()
LoadAsync().Wait() في معالج الواجهة كذلك. تتوقّف حلقة الرسائل await LoadAsync()
LoadAsync().GetAwaiter().GetResult() في معالج الواجهة شكل ظهور الاستثناء مختلف فقط، والحجب واحد await LoadAsync()
ConfigureAwait(false) آليّاً في شيفرة الواجهة يسهل انكسار تحديث الواجهة بعد await الطبقة الخارجيّة للواجهة plain await
Task.Run(async () => await IoAsync()) إعادة إلقاء I/O بلا جدوى await IoAsync()
شيفرة المكتبة تمسك Dispatcher أو Control مباشرة يتعمّق الاعتماد على الواجهة. يصعب إعادة الاستخدام المكتبة تعيد بيانات فقط، وجانب الواجهة يقوم بـ marshal
الإكثار من Dispatcher.Invoke / Control.Invoke في تدفّق async يسهل تكوّن حلقة حجب النظر في Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync
مزامنة async قسراً من منشئ أو جالب خاصّيّة مرتع تعليق عند التشغيل الإفلات إلى Loaded / Shown / InitializeAsync

من هذه، معدّل المواجهة أعلى خصوصاً في ثلاث.

  1. .Result / .Wait() على خيط الواجهة
  2. وضع ConfigureAwait(false) آليّاً في شيفرة الواجهة
  3. اختلاط مسؤوليّة المكتبة والواجهة فيتغلغل Dispatcher إلى العمق

إخراج هذه الثلاث وحدها يهدّئ الشيفرة كثيراً.

ثلاث ذات معدّل مواجهة أعلى خصوصاًيبيّن أنّ إخراج ثلاث — .Result أو .Wait على خيط الواجهة، وConfigureAwait(false) الآليّ في شيفرة الواجهة، وتغلغل Dispatcher إلى عمق المكتبة باختلاط المسؤوليّة — يهدّئ الشيفرة..Result أو .Wait على خيط الواجهةإخراج هذه الثلاثConfigureAwait الآليّتغلغل Dispatcher إلى العمقتهدئة الشيفرة كثيراً

الشكل 19: من الأنماط المضادّة، معدّل المواجهة أعلى في هذه الثلاث، وإخراجها وحدها أثره كبير.

7. قائمة فحص عند المراجعة

المحتوى نفسه جدول الحكم في 2.2 والأنماط المضادّة في الفصل 6، لكنّ هذا شكل أسئلة تُؤكَّد بالترتيب عند فتح الشيفرة.

  • هل بقي .Result / .Wait() / .GetAwaiter().GetResult() في معالج حدث الواجهة أو مسار تهيئة الواجهة
  • هل Task.Run يُستخدَم لحساب CPU فقط. هل لا يلفّ I/O
  • هل لم يدخل ConfigureAwait(false) آليّاً في شيفرة الواجهة
  • بالمقابل، هل لا تجرّ المكتبة العامّة اعتماداً على سياق الواجهة
  • مواضع لمس الواجهة مباشرة بعد await، هل يمكن القول إنّها حقّاً على سياق الواجهة
  • في المواضع التي يلزم فيها الإعادة الصريحة إلى الواجهة، هل يُستخدَم Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync
  • هل لم تزد marshal متزامنة مثل Dispatcher.Invoke / Control.Invoke بلا لزوم
  • هل لم تُزامَن async قسراً من منشئ أو خاصّيّة متزامنة أو حدث متزامن
  • هل طبقة المكتبة لا تشير مباشرة إلى Window / Control / Dispatcher

قائمة الفحص هذه سهلة الاستخدام أيضاً لمواءمة «أين مسؤوليّة الواجهة» في الفريق.

8. دليل اختيار تقريبيّ

اختيار كلّ وضع مُجمَّع في جدول الحكم في 2.2، لذا نضع هنا طريقة تذكّر للحمل فقط.

  • الطبقة الخارجيّة للواجهة plain await. إمكان لمس الواجهة كما هو بعد await لأنّ هذا محفوظ
  • Task.Run وجهة إفلات CPU. ليست أداة للفّ انتظار I/O
  • ConfigureAwait(false) أداة المكتبة العامّة. لا تُوضَع آليّاً في شيفرة الواجهة
  • Dispatcher / BeginInvoke / InvokeAsync فقط عند لمس الواجهة من موضع غير الواجهة
  • لا تُستخدَم ثلاث الانتظار على خيط الواجهة (.Result / .Wait() / .GetAwaiter().GetResult()). إذا أُريدت المزامنة يُمَدّ المستدعي كلّه إلى async

جزء الأسباب في الفصل 4، واختيار Dispatcher / Invoke في الفصل 5، وزوايا الإيجاد في الشيفرة الفعليّة في الفصلين 6 و7.

9. الخلاصة

ما يهمّ حقّاً في async / await في WPF / WinForms ليس جوّ «اللاتزامن صعب»، بل التفكير منفصلاً في

  • أين بدأ الأمر الآن
  • إلى أين تعود تكملة await
  • من يحمل مسؤوليّة الإعادة إلى الواجهة

كقاعدة أوّل، حفظ هذا القدر يكفي للقتال.

  1. في الطبقة الخارجيّة للواجهة plain await
  2. Task.Run لـ CPU الثقيل فقط
  3. في المكتبة العامّة النظر في ConfigureAwait(false)
  4. Dispatcher / BeginInvoke / InvokeAsync فقط عند لزوم الإعادة إلى الواجهة
  5. على خيط الواجهة لا يُستخدَم .Result / .Wait() / .GetAwaiter().GetResult()

async / await نفسه ليس آليّة صعبة المزاج إلى ذلك الحدّ. لكنّه إذا استُخدم دون النظر متمحوراً حول خيط الواجهة، يصير وحلاً فجأة.

بعبارة معكوسة،

  • فصل خارج الواجهة عن داخلها
  • الوعي بوجهة العودة
  • عدم إدخال الحجب

حفظ هذه الثلاث وحدها يهدّئ الشيفرة اللاتزامنيّة في WPF / WinForms كثيراً. الشيفرة التي تتجمّد فيها الشاشة ليست عادة «اللاتزامن سيّئ»، بل فقط طريقة الاستدانة من خيط الواجهة فوضويّة.

ثلاثة مبادئ لشيفرة واجهة لاتزامنيّة هادئةيبيّن أنّ حفظ ثلاث — فصل خارج الواجهة عن داخلها، والوعي بوجهة عودة await، وعدم إدخال الحجب — يهدّئ الشيفرة اللاتزامنيّة في WPF وWinForms.فصل خارج الواجهة عن داخلهاتهدئة الشيفرة اللاتزامنيّةالوعي بوجهة العودةعدم إدخال الحجب

الشكل 20: ثلاثة مبادئ الخلاصة. الفصل والوعي بوجهة العودة وعدم الحجب، فلا تتجمّد الشاشة.

10. روابط مرجعيّة

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

إلى أيّ خيط نعود بعد await؟
إذا استُخدم plain await (بلا ConfigureAwait) في معالج حدث واجهة WPF / WinForms، فتكملة ما بعد await تعود أساساً إلى خيط الواجهة. لأنّ await يمسك SynchronizationContext الواجهة في تلك اللحظة ويعيد الاستكمال إليه، يمكن كتابة تحديث TextBox أو Label بعد await كما هو. حتّى مع await Task.Run(...) يعمل جسم الحساب على ThreadPool، لكنّ التكملة مع plain await تُستأنف على خيط الواجهة.
لماذا تتجمّد الواجهة عند استخدام .Result أو .Wait() على خيط الواجهة؟
بينما ينتظر خيط الواجهة بـ .Result، يحاول استكمال المعالجة اللاتزامنيّة العودة إلى سياق الواجهة الملتقَط، لكنّ خيط الواجهة مسدود بـ .Result فلا يمكن تنفيذ الاستكمال، فينتظر كلّ منهما الآخر ويحدث deadlock. GetAwaiter().GetResult() يختلف في تغليف الاستثناء فقط، وجوهر سدّ خيط الواجهة واحد. في الواجهة تُتجنَّب الثلاثة .Result و.Wait() وGetAwaiter().GetResult() ويُستخدَم await.
هل ينبغي وضع ConfigureAwait(false) في شيفرة الواجهة؟
الأفضل ألا يُوضَع. ConfigureAwait(false) يعني «عدم فرض العودة إلى سياق الواجهة الملتقَط»، لذا قد تُستأنف التكملة على أيّ خيط، وقد يصير تحديث الواجهة فور ذلك وصولاً عبر الخيوط. ما يناسبه شيفرة مكتبة عامّة لا تعتمد على الواجهة، والسياسة إبقاء الطبقة الخارجيّة للواجهة plain await.
متى ينبغي استخدام Task.Run؟
فقط عندما يُراد إخراج حساب CPU ثقيل من خيط الواجهة. لفّ انتظار I/O بـ Task.Run يعيد إلقاء الانتظار على ThreadPool بلا جدوى. داخل Task.Run فقط هو الخيط الآخر، وتكملة await Task.Run(...) مع plain await تعود عادة إلى خيط الواجهة، لذا يمكن كتابة انعكاس النتيجة على الشاشة كما هو.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة