سجل التعديلات (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)
المحتويات
- الخلاصة أوّلاً (في جملة)
- التنظيم أوّلاً في صفحة واحدة
- 2.1. الصورة الكلّيّة
- 2.2. جدول الحكم أوّلاً
- مصطلحات هذا المقال
- 3.1. خيط الواجهة وحلقة الرسائل
- 3.2.
SynchronizationContext/Dispatcher/Invoke
- الأنماط النموذجيّة
- 4.1. plain
awaitفي معالج حدث الواجهة - 4.2.
Task.Runلحساب CPU الثقيل فقط - 4.3.
ConfigureAwait(false)ليس «ضمان عدم العودة» بل «عدم فرض العودة» - 4.4. سبب التوقّف في
.Result/.Wait()/.GetAwaiter().GetResult()
- 4.1. plain
- متى يُستخدَم
Dispatcher/Invoke - أنماط مضادّة شائعة
- قائمة فحص عند المراجعة
- دليل اختيار تقريبيّ
- الخلاصة
- روابط مرجعيّة
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 26، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
1. الخلاصة أوّلاً (في جملة)
- إذا استُخدم plain
awaitفي معالج حدث الواجهة في WPF / WinForms، فتكملة ما بعدawaitتعود أساساً إلى خيط الواجهة Task.Runلإخراج حساب CPU من خيط الواجهة، وليس أداة للفّ انتظار I/O- حتّى مع
await Task.Run(...)داخل معالج الواجهة، إذا كان ذلكawaitهو plainawaitفالتكملة تعود عادة إلى خيط الواجهة ConfigureAwait(false)يعني عدم فرض العودة إلى سياق الواجهة الملتقَط في ذلكawait. لمس الواجهة مباشرة في التكملة بعده خطر.Result/.Wait()/.GetAwaiter().GetResult()يسدّ خيط الواجهة. إذا احتاج استكمالawaitالعودة إلى الواجهة، يتوقّف الأمر بسهولة- للإعادة الصريحة إلى الواجهة في WPF:
Dispatcher.InvokeAsync - للإعادة الصريحة إلى الواجهة في WinForms: سابقاً
BeginInvoke، ومن .NET 9 فصاعداًInvokeAsyncينسجم مع تدفّق async - السياسة أوّلاً: الطبقة الخارجيّة للواجهة plain
await، والمكتبة العامّة تنظر فيConfigureAwait(false)، والإعادة إلى الواجهة تُصرَّح بها في المواضع اللازمة فقط
باختصار، في WPF / WinForms
- على أيّ خيط نعمل الآن
- إلى أين تعود تكملة
await - من يحمل مسؤوليّة الإعادة إلى الواجهة
ضبط هذه الثلاث يحسّن الرؤية كثيراً.
flowchart TB
accTitle: ثلاثة أسئلة تحسّن الرؤية
accDescr: يبيّن أنّ ضبط ثلاث: على أيّ خيط نعمل الآن، وإلى أين تعود تكملة await، ومن يحمل مسؤوليّة الإعادة إلى الواجهة، يحسّن رؤية الشيفرة اللاتزامنيّة في WPF وWinForms.
q1["على أيّ خيط نعمل الآن"] --> goal["رؤية شيفرة الواجهة اللاتزامنيّة"]
q2["إلى أين تعود تكملة await"] --> goal
q3["من يحمل مسؤوليّة الإعادة إلى الواجهة"] --> goal
الشكل 1: ثلاثة أسئلة يُرجَع إليها عند الحيرة. الخيط ووجهة العودة ومسؤوليّة الإعادة تُفكَّر منفصلة.
2. التنظيم أوّلاً في صفحة واحدة
2.1. الصورة الكلّيّة
الإمساك بالصورة الكلّيّة بهذا الرسم أسرع أوّلاً.
flowchart LR
A["معالج حدث الواجهة<br/>(WPF / WinForms)"] --> B["plain await<br/>I/O API"]
B --> C["إمساك UI SynchronizationContext"]
C --> D["الاستئناف بعد await على خيط الواجهة"]
D --> E["يمكن كتابة تحديث الواجهة كما هو"]
A --> F["await Task.Run(...)<br/>معالجة CPU ثقيلة"]
F --> G["جسم الحساب على ThreadPool"]
G --> H["الاستئناف بعد await على خيط الواجهة"]
H --> E
A --> I["await SomeAsync().ConfigureAwait(false)"]
I --> J["عدم فرض العودة إلى الواجهة"]
J --> K["التكملة على أيّ خيط"]
K --> L["تحديث الواجهة مباشرة خطر<br/>يلزم Dispatcher / Invoke"]
A --> M["SomeAsync().Result / Wait()<br/>GetAwaiter().GetResult()"]
M --> N["حجب خيط الواجهة"]
N --> O["الاستكمال لا يستطيع العودة إلى الواجهة"]
O --> P["تعليق / deadlock / تجمّد على الأقلّ"]
الشكل 2: الصورة الكلّيّة لأربعة أنماط من معالج الواجهة. plain await وTask.Run يعودان إلى الواجهة، وConfigureAwait(false) لا يفرض العودة، و.Result / .Wait() يسدّ خيط الواجهة.
ما يُشاهَد في العمل تقريباً هذه الأنماط الأربعة.
- plain
awaitفي معالج حدث الواجهة - إفلات CPU بـ
Task.Runفي معالج حدث الواجهة - إخراج وجهة العودة بـ
ConfigureAwait(false) - سدّ خيط الواجهة بـ
.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).
flowchart TB
accTitle: العدو ليس await بل الحجب التزامنيّ
accDescr: يبيّن الترتيب أنّ plain await في شيفرة الواجهة حليف بالأحرى، وأنّ العدو الحقيقيّ سدّ خيط الواجهة تزامنيّاً.
pa["plain await"] --> friend["حليف بالأحرى في شيفرة الواجهة"]
blk["سدّ خيط الواجهة تزامنيّاً"] --> enemy["مصدر التجمّد وdeadlock"]
الشكل 3: الترتيب في لبّ جدول الحكم. العدو ليس await نفسه، بل سدّ خيط الواجهة تزامنيّاً.
3. مصطلحات هذا المقال
3.1. خيط الواجهة وحلقة الرسائل
واجهة WPF / WinForms أساساً شكل خيط واجهة واحد يدير الإدخال والرسم ومعالجة الأحداث.
دور خيط الواجهة هذا تقريباً هكذا.
- معالجة رسائل مثل ضغط الزرّ وإدخال المفاتيح وإعادة الرسم
- الخيط الوحيد الذي يمكنه لمس عناصر التحكّم وكائنات الواجهة بأمان
- إذا حُشيت فيه معالجة أكثر ممّا يلزم، يتوقّف تحديث الشاشة واستجابة الإدخال
اللبّ هنا أنّ عمل خيط الواجهة «الدوران بسرعة». إذا حُجب هنا طويلاً، يتوقّف الفأرة ولوحة المفاتيح وإعادة الرسم، فيبدو للمستخدم «تجمّد».
وضع هذه الصورة في الرأس كرسم يقلّل الاختلاط.
flowchart LR
A["إدخال المستخدم / طلب إعادة الرسم"] --> B["حلقة رسائل خيط الواجهة"]
B --> C["تنفيذ معالج الحدث"]
C --> D["تحديث الشاشة"]
D --> B
C --> E["معالجة متزامنة طويلة"]
E --> F["حلقة الرسائل لا تدور"]
F --> G["الشاشة تبدو متجمّدة"]
الشكل 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 الواجهة نافذ.
flowchart TB
accTitle: كيف تُقرَّر وجهة استكمال await
accDescr: يبيّن أنّ await الافتراضيّ يمسك أوّلاً SynchronizationContext.Current، وإذا كان null ينظر إلى TaskScheduler.Current، فإن لم يكن الافتراضيّ يعيد إلى ذلك TaskScheduler، وإلا يعمل الاستكمال على ThreadPool.
a["await الافتراضيّ"] --> sc{"هل يوجد SynchronizationContext؟"}
sc -->|"يوجد"| toSc["الإعادة إلى ذلك السياق"]
sc -->|"null"| ts{"هل TaskScheduler افتراضيّ؟"}
ts -->|"ليس الافتراضيّ"| toTs["الإعادة إلى ذلك TaskScheduler"]
ts -->|"افتراضيّ"| pool["تنفيذ الاستكمال على ThreadPool"]
toSc -.-> ui["على خيط الواجهة سياق الواجهة"]
الشكل 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.
في العمل، تذكّر علاقة التجريد بالواقع بهذا القدر يقلّل الاختلاط.
flowchart TD
A["الشيفرة الحاليّة"] --> B["SynchronizationContext"]
B --> C["WPF: DispatcherSynchronizationContext"]
B --> D["WinForms: WindowsFormsSynchronizationContext"]
C --> E["Dispatcher.InvokeAsync / BeginInvoke / Invoke"]
D --> F["Control.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 وغيرها) تُوضَع كشبكة أخيرة فقط.
flowchart TB
accTitle: وجهة استثناء async void
accDescr: يبيّن أنّ استثناء async Task يركب على Task القيمة المعادة فيستلمه المستدعي بـ await، أمّا في async void فيُعاد رمي الاستثناء الذي خرج إلى SynchronizationContext عند البدء أي خيط الواجهة، وإذا لم يُعالَج في الشبكة الكلّيّة يسقط التطبيق.
ex["استثناء خرج من المعالج"] --> kind{"async Task أم async void؟"}
kind -->|"async Task"| task["يركب على Task القيمة المعادة"]
task --> caller["المستدعي الذي استخدم await يستلمه"]
kind -->|"async void"| ctx["يُعاد رميه إلى خيط الواجهة"]
ctx --> global["يخرج إلى الشبكة الكلّيّة"]
global --> crash["إن لم يُعالَج يسقط التطبيق"]
الشكل 7: ليس لـ async void Task يحمل الاستثناء، لذا الأساس الإمساك داخل المعالج.
في WinForms النظرة نفسها.
ما دام plain await داخل معالج Click، فالتكملة تعود أساساً إلى جانب الواجهة.
كرسم، التدفّق هكذا.
sequenceDiagram
participant UI as خيط الواجهة
participant IO as I/O لاتزامنيّ
participant Ctx as UI SynchronizationContext
UI->>UI: بدء معالج Click
UI->>IO: await لـ ReadAllTextAsync
UI-->>Ctx: حجز إعادة التكملة إلى الواجهة
Note over UI: أثناء الانتظار العودة إلى حلقة الرسائل
IO-->>Ctx: اكتمال I/O
Ctx-->>UI: استئناف التكملة على خيط الواجهة
UI->>UI: تحديث 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;
}
}
ما يحدث في هذه الشيفرة تقريباً هكذا.
- يبدأ معالج الحدث على خيط الواجهة
- انتظار I/O لـ
File.ReadAllBytesAsyncيُمرَّر لاتزامنيّاً - حساب التجزئة الثقيل فقط يُخرَج إلى ThreadPool بـ
Task.Run - تكملة
await Task.Run(...)plainawaitفتعود إلى خيط الواجهة - يمكن كتابة
ResultText.Text = hash;كما هو
أي أنّ داخل Task.Run فقط هو الخيط الآخر.
ليس أنّه يذهب بشكل دائم إلى «موضع لم يعد الواجهة» حتّى بعد await.
النظر إلى هذا في صفحة واحدة يقلّل سوء الفهم.
sequenceDiagram
participant UI as خيط الواجهة
participant IO as I/O لاتزامنيّ
participant Pool as ThreadPool
UI->>IO: await لـ ReadAllBytesAsync
IO-->>UI: plain await فيُستأنف على الواجهة
UI->>Pool: إلقاء معالجة CPU الثقيلة بـ Task.Run
Pool-->>UI: إعادة نتيجة الحساب
Note over UI: تكملة await Task.Run(...) تُستأنف على الواجهة
UI->>UI: انعكاس النتيجة على الشاشة
الشكل 9: داخل Task.Run فقط يعمل على ThreadPool، وتكملة await تعود إلى خيط الواجهة لذا يُكتَب انعكاس الشاشة كما هو.
هنا تنبيهان.
- لا تُلفّ انتظار I/O بـ
Task.Run - اعتبر
Task.Runليس «جعل الأمر لاتزامنيّاً» بل صنع «وجهة إفلات CPU»
كتابة مثل Task.Run(async () => await File.ReadAllTextAsync(...)) تعيد إلقاء انتظار I/O على ThreadPool بلا جدوى، فلا نفع يُذكَر.
flowchart TB
accTitle: موضع استخدام Task.Run
accDescr: يبيّن أنّ دور Task.Run إفلات حساب CPU الثقيل إلى ThreadPool، وأنّ لفّ انتظار I/O يعيد إلقاء الانتظار على ThreadPool بلا جدوى.
q{"ما المراد إفلاته؟"}
q -->|"حساب CPU ثقيل"| ok["الإفلات بـ Task.Run"]
q -->|"انتظار I/O"| ng["لا يُلفّ بـ Task.Run"]
ng --> why["إعادة إلقاء الانتظار بلا جدوى"]
الشكل 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لذلك، فتكملة المستدعي تعود إلى الواجهة
flowchart TB
accTitle: فصل المكتبة عن الواجهة
accDescr: يبيّن الفصل أنّ await داخل المكتبة العامّة لا يطلب العودة إلى سياق الواجهة بـ ConfigureAwait(false)، وأنّ معالج الواجهة إذا استخدم plain await لذلك فتكملة المستدعي تعود إلى الواجهة.
lib["await داخل المكتبة"] --> nof["ConfigureAwait (false)"]
nof --> stay["لا يطلب العودة إلى الواجهة"]
uih["plain await في معالج الواجهة"] --> back["تكملة المستدعي تعود إلى الواجهة"]
nof -.-> note["تعيين الداخل لا يمتدّ إلى الخارج"]
الشكل 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 إلى سياق الواجهة الأصليّ. هذا فقط.
كرسم هكذا.
flowchart LR
A["await في معالج الواجهة"] --> B{"هل يُوضَع ConfigureAwait(false)؟"}
B -- لا --> C["التكملة أساساً خيط الواجهة"]
C --> D["يسهل تحديث الواجهة كما هو"]
B -- نعم --> E["التكملة لا تُثبَّت على الواجهة"]
E --> F["قد تُستأنف على أيّ خيط"]
F --> G["تحديث الواجهة يلزم 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();
}
للوهلة الأولى يبدو مجرّد أخذ النتيجة تزامنيّاً، لكنّه خطر على خيط الواجهة.
التدفّق كرسم هكذا.
sequenceDiagram
participant UI as خيط الواجهة
participant IO as I/O لاتزامنيّ
participant Ctx as UI SynchronizationContext
UI->>UI: بدء LoadButton_Click
UI->>IO: استدعاء LoadTextAsync()
IO-->>UI: إعادة Task غير مكتمل
UI->>UI: الانتظار والحجب بـ .Result
IO-->>Ctx: اكتمال I/O، إرادة إعادة الاستكمال إلى الواجهة
Ctx-->>UI: إرادة تنفيذ التكملة
Note over UI: لكنّ الواجهة مسدودة بـ .Result
Note over UI, Ctx: الاستكمال لا يدور فلا يكتمل
الشكل 13: تدفّق لا يستطيع فيه الاستكمال العودة إلى خيط الواجهة المسدود بـ .Result، فلا يكتمل Task أبداً.
ما يحدث بكلمات هكذا.
- خيط الواجهة يستدعي
LoadTextAsync() awaitداخلLoadTextAsync()يمسك سياق الواجهة- خيط الواجهة ينتظر بـ
.Result - يكتمل I/O
- تكملة
LoadTextAsync()تريد العودة إلى خيط الواجهة - لكنّ خيط الواجهة مسدود بـ
.Result - لا تعمل التكملة فلا تكتمل
LoadTextAsync() .Resultلا ينتهي
أي أنّ الواجهة تقول «أنتظر حتّى تنتهي»، وجانب اللاتزامن يقول «أنتهي إذا عدت إلى الواجهة»، فينتظر كلّ منهما الآخر. شعور كريه حقّاً.
flowchart TB
accTitle: تركيب انتظار متبادل
accDescr: يبيّن أنّ خيط الواجهة ينتظر اكتمال المعالجة اللاتزامنيّة بـ .Result، واستكمال الجانب اللاتزامنيّ ينتظر فراغ خيط الواجهة، فينتظر كلّ منهما الآخر فلا يتقدّم الأمر.
ui["خيط الواجهة ينتظر بـ .Result"] --> need["يلزم اكتمال الجانب اللاتزامنيّ"]
cont["الاستكمال يلزم العودة إلى الواجهة"] --> free["يلزم فراغ خيط الواجهة"]
need --> cycle["انتظار متبادل فلا يتقدّم الأمر"]
free --> cycle
الشكل 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!.
flowchart TB
accTitle: خطر انتظار InvokeAsync تزامنيّاً
accDescr: يبيّن أنّ Dispatcher.InvokeAsync يكدّس المفوَّض في الطابور فقط، والتنفيذ عندما يدير خيط الواجهة ذلك الطابور، فإذا توقّف خيط الواجهة بـ Wait لا يدور الطابور فلا يكتمل Task أبداً.
post["التكديس في الطابور بـ InvokeAsync"] --> run["التنفيذ عندما يدير خيط الواجهة الطابور"]
wait["توقّف خيط الواجهة بـ Wait"] --> norun["الطابور لا يدور"]
norun --> never["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، جانب عدم الحجب أساساً أنسب للانسجام.
flowchart TB
accTitle: الفرق بين Invoke وBeginInvoke
accDescr: يبيّن أنّ Invoke إرسال متزامن ينتظر المستدعي، وأنّ BeginInvoke ينشر ويعود فوراً، لذا في تدفّق async جانب عدم الحجب أنسب للانسجام.
inv["Invoke (إرسال متزامن)"] --> waitc["ينتظر المستدعي"]
bi["BeginInvoke (نشر)"] --> ret["يعود فوراً"]
ret --> fit["ينسجم مع تدفّق 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.ReadAllTextAsyncAPI من .NET Core 2.0 فصاعداً. للشكل نفسه على .NET Framework 4.8 استبدله بـStreamReader.ReadToEndAsyncأو نحوه
flowchart TB
accTitle: تنازع حقّ الإلغاء والتنفيذ
accDescr: يبيّن منع سباق يعيد فيه المفوَّض القديم كتابة الشاشة بتنازع جانب الإلغاء وجانب التنفيذ على حقّ مرّة واحدة بـ Interlocked.Exchange، فيتقدّم من أخذه أوّلاً فقط، ويعود من لم يأخذه دون فعل شيء.
race["حقّ مرّة واحدة"] --> c["جانب الإلغاء يأخذه أوّلاً"]
race --> e["جانب التنفيذ يأخذه أوّلاً"]
c --> c2["طيّ Task كملغى"]
c --> c3["المفوَّض القديم يعود دون فعل شيء"]
e --> e2["تنفيذ 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
بهذا تقلّ الحوادث كثيراً.
عند الحيرة يكفي رسم حكم بهذا القدر.
flowchart TD
A["موضع كتابة هذه التكملة خيط الواجهة؟"] --> B{"نعم؟"}
B -- نعم --> C["تحديث الواجهة بـ plain await كما هو جائز"]
B -- لا --> D{"هل يُراد لمس الواجهة؟"}
D -- لا --> E["متابعة المعالجة كما هي"]
D -- نعم --> F["WPF: Dispatcher.InvokeAsync"]
D -- نعم --> G["WinForms: 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 |
من هذه، معدّل المواجهة أعلى خصوصاً في ثلاث.
.Result/.Wait()على خيط الواجهة- وضع
ConfigureAwait(false)آليّاً في شيفرة الواجهة - اختلاط مسؤوليّة المكتبة والواجهة فيتغلغل
Dispatcherإلى العمق
إخراج هذه الثلاث وحدها يهدّئ الشيفرة كثيراً.
flowchart TB
accTitle: ثلاث ذات معدّل مواجهة أعلى خصوصاً
accDescr: يبيّن أنّ إخراج ثلاث — .Result أو .Wait على خيط الواجهة، وConfigureAwait(false) الآليّ في شيفرة الواجهة، وتغلغل Dispatcher إلى عمق المكتبة باختلاط المسؤوليّة — يهدّئ الشيفرة.
a1[".Result أو .Wait على خيط الواجهة"] --> fix["إخراج هذه الثلاث"]
a2["ConfigureAwait الآليّ"] --> fix
a3["تغلغل Dispatcher إلى العمق"] --> fix
fix --> calm["تهدئة الشيفرة كثيراً"]
الشكل 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/OConfigureAwait(false)أداة المكتبة العامّة. لا تُوضَع آليّاً في شيفرة الواجهةDispatcher/BeginInvoke/InvokeAsyncفقط عند لمس الواجهة من موضع غير الواجهة- لا تُستخدَم ثلاث الانتظار على خيط الواجهة (
.Result/.Wait()/.GetAwaiter().GetResult()). إذا أُريدت المزامنة يُمَدّ المستدعي كلّه إلى async
جزء الأسباب في الفصل 4، واختيار Dispatcher / Invoke في الفصل 5، وزوايا الإيجاد في الشيفرة الفعليّة في الفصلين 6 و7.
9. الخلاصة
ما يهمّ حقّاً في async / await في WPF / WinForms ليس
جوّ «اللاتزامن صعب»، بل التفكير منفصلاً في
- أين بدأ الأمر الآن
- إلى أين تعود تكملة
await - من يحمل مسؤوليّة الإعادة إلى الواجهة
كقاعدة أوّل، حفظ هذا القدر يكفي للقتال.
- في الطبقة الخارجيّة للواجهة plain
await Task.Runلـ CPU الثقيل فقط- في المكتبة العامّة النظر في
ConfigureAwait(false) Dispatcher/BeginInvoke/InvokeAsyncفقط عند لزوم الإعادة إلى الواجهة- على خيط الواجهة لا يُستخدَم
.Result/.Wait()/.GetAwaiter().GetResult()
async / await نفسه ليس آليّة صعبة المزاج إلى ذلك الحدّ.
لكنّه إذا استُخدم دون النظر متمحوراً حول خيط الواجهة، يصير وحلاً فجأة.
بعبارة معكوسة،
- فصل خارج الواجهة عن داخلها
- الوعي بوجهة العودة
- عدم إدخال الحجب
حفظ هذه الثلاث وحدها يهدّئ الشيفرة اللاتزامنيّة في WPF / WinForms كثيراً. الشيفرة التي تتجمّد فيها الشاشة ليست عادة «اللاتزامن سيّئ»، بل فقط طريقة الاستدانة من خيط الواجهة فوضويّة.
flowchart TB
accTitle: ثلاثة مبادئ لشيفرة واجهة لاتزامنيّة هادئة
accDescr: يبيّن أنّ حفظ ثلاث — فصل خارج الواجهة عن داخلها، والوعي بوجهة عودة await، وعدم إدخال الحجب — يهدّئ الشيفرة اللاتزامنيّة في WPF وWinForms.
r1["فصل خارج الواجهة عن داخلها"] --> calm["تهدئة الشيفرة اللاتزامنيّة"]
r2["الوعي بوجهة العودة"] --> calm
r3["عدم إدخال الحجب"] --> calm
الشكل 20: ثلاثة مبادئ الخلاصة. الفصل والوعي بوجهة العودة وعدم الحجب، فلا تتجمّد الشاشة.
10. روابط مرجعيّة
- مجموعة عيّنات هذا المقال (مكتبة لا تعتمد على الواجهة، عيّنات WPF / WinForms، اختبارات وحدة) - komurasoft-blog-samples (GitHub)
- مقال مرتبط: جدول حكم عمليّ لـ async/await في C# - Task.Run وConfigureAwait
- Threading Model - WPF
- DispatcherSynchronizationContext Class
- How to handle cross-thread operations with controls - Windows Forms
- WindowsFormsSynchronizationContext Class
- Events Overview - Windows Forms
- TaskScheduler.FromCurrentSynchronizationContext Method
- ConfigureAwait FAQ
- How Async/Await Really Works in C#
- Await, and UI, and deadlocks! Oh my!
- Threading model for WebView2 apps
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
CI/CD عمليّ لتطبيقات WinForms / WPF ── أتمتة البناء والتوقيع والتوزيع عبر GitHub Actions
دليل عمليّ لبناء CI/CD لتطبيقات WinForms / WPF عبر GitHub Actions. يشمل YAML الأدنى للبناء والاختبار على windows-latest، وترقيم الإصدار ا...
إقامة تطبيقات Windows في علبة النظام والإشعارات المنبثقة ── مطبات NotifyIcon وكيفية اختيار AppNotification
نرتّب إقامة تطبيقات Windows للأعمال في علبة النظام والإشعارات المنبثقة. نشرح الاستخدام الصحيح لـ NotifyIcon، وإعادة التسجيل عند إعادة تشغ...
تعدد اللغات في تطبيقات WinForms/WPF ── الممارسة العملية لملفات resx وتجميعات الأقمار الصناعية وتبديل الثقافة
نرتّب تعدد اللغات في تطبيقات سطح المكتب على Windows: الفرق بين CurrentCulture وCurrentUICulture، وآلية resx وتجميعات الأقمار الصناعية، وا...
دمج مصادقة Entra ID في تطبيقات WinForms/WPF ── التشكيل العملي لـ MSAL.NET ووسيط WAM
نرتّب خطوات دمج مصادقة Entra ID في تطبيقات WinForms/WPF لسطح المكتب. نشرح مفهوم العميل العامّ، وتسجيل التطبيق، وAcquireTokenSilent في MSA...
الاختبار الآلي لواجهة المستخدم في تطبيقات سطح مكتب Windows ── آلية UI Automation وبناء اختبارات لا تنكسر بسهولة باستخدام FlaUI
نرتّب الاختبار الآلي لواجهة المستخدم في تطبيقات WinForms/WPF انطلاقاً من آلية Windows UI Automation. نشرح التنفيذ الأدنى عبر FlaUI، وتصمي...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
خيط واجهة المستخدم في WPF / WinForms مع async/await من أكثر النقاط التي يتعثّر عندها التنفيذ في تطوير تطبيقات Windows.
الاستشارات التقنية ومراجعة التصميم
إن كنت في مرحلة تريد فيها ترتيب مسؤوليات واجهة المستخدم والمعالجة الخلفية ومتى يُستخدم Dispatcher، فيمكن إعادة النظر في ذلك ضمن الاستشارة التقنية ومراجعة التصميم.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- إلى أيّ خيط نعود بعد 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 تعود عادة إلى خيط الواجهة، لذا يمكن كتابة انعكاس النتيجة على الشاشة كما هو.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.