أساسيات COM STA/MTA - نموذج الخيوط وتجنّب التعليق

· آخر تحديث: · · COM, تطوير Windows, STA, MTA, خيوط

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

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

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

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

小村 豪 (2026). أساسيات COM STA/MTA - نموذج الخيوط وتجنّب التعليق. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621307 https://comcomponent.com/ar/blog/2026/01/31/000-sta-mta-com-relationship/

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

STA/MTA في COM معرفة أساسية يصعب تجنّبها عند تطوير Windows أو لمس COM من .NET. وخصوصاً ما يكثر في البحث: لماذا خيط واجهة المستخدم STA، وماذا يحدث عند عبور Apartment، ولماذا يحدث تعليق.

المحتويات


عند استخدام COM لا مفرّ من «على أيّ خيط يعمل». في صميم ذلك نموذج Apartment (STA/MTA). STA/MTA ليسا مفهوم خيوط Windows العام، بل نموذج خيوط لتحديد قواعد استدعاء كائنات COM.

يربط هذا المقال علاقة STA وMTA وCOM بالرسوم، ويصل حتى «لماذا قد يحدث تعليق».

موضع نموذج Apartmentمخطّط يبيّن أنّ STA/MTA ليسا مفهوم خيوط Windows العام، بل الشكلان لنموذج Apartment، وهو نموذج خيوط لتحديد قواعد استدعاء كائنات COM.قواعد استدعاء كائن COMنموذج ApartmentSTAMTAليس مفهوم خيوط Windows العام

الشكل 1: STA/MTA هما الشكلان لنموذج Apartment الذي يحدّد قواعد استدعاء كائنات COM.

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

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

  • قواعد استدعاء كائن COM تُحسم بحسب «أيّ Apartment ينتمي إليه»
  • يسهل الفهم إذا رأيت STA شقّة واحدة لكلّ خيط، وMTA شقّة واحدة لعدّة خيوط
  • الاستدعاء الذي يعبر Apartment يجري له COM marshalling عبر Proxy/Stub
علاقة STA وMTA وعبور Apartmentمخطّط يبيّن الخلاصة: STA شكل شقّة واحدة لكلّ خيط، وMTA شكل شقّة واحدة لعدّة خيوط، والاستدعاء الذي يعبر Apartment يجري له COM marshalling عبر Proxy/Stub.استدعاء يعبر Apartmentاستدعاء يعبر ApartmentSTA (شقّة واحدة لكلّ خيط)marshalling عبر Proxy / StubMTA (شقّة واحدة لعدّة خيوط)

الشكل 2: الشقّة التي ينتمي إليها الكائن تحدّد قواعد الاستدعاء، ولا يُقحم marshalling إلا عند عبور Apartment.

2. أنماط الاستدعاء في نموذج Apartment (رسوم)

لاستدعاء كائن COM ثلاثة أنماط رئيسة.

الأنماط الثلاثة للاستدعاءمخطّط يبيّن أنّ لاستدعاء كائن COM ثلاثة أنماط: الاستدعاء داخل خيط STA نفسه، والاستدعاء داخل MTA نفسه، والاستدعاء الذي يعبر Apartment.استدعاء كائن COMالنمط 1: داخل خيط STA نفسهالنمط 2: داخل MTA نفسهالنمط 3: عبور Apartment

الشكل 3: تنقسم أنماط الاستدعاء إلى ثلاثة، وبحسب أيّها ينطبق تتغيّر التكلفة ونقاط الانتباه.

2.1. النمط 1: الاستدعاء داخل خيط STA نفسه

داخل خيط STA نفسه يمكن الاستدعاء مباشرة. بلا تكلفة إضافية.

خيط STAاستدعاء مباشرشيفرة المستدعيكائن COM

الشكل 4: الاستدعاء داخل خيط STA نفسه استدعاء مباشر، بلا تكلفة إضافية.

2.2. النمط 2: الاستدعاء داخل MTA نفسه

من عدّة خيوط داخل MTA يمكن الاستدعاء مباشرة من أيّ خيط. غير أنّ جانب الكائن يحتاج تصميم thread-safe إلزاماً.

MTA (شقّة واحدة)استدعاء مباشراستدعاء مباشرخيط عامل 1كائن COMخيط عامل 2

الشكل 5: داخل MTA نفسه يمكن الاستدعاء مباشرة من أيّ خيط، لكن يلزم تصميم thread-safe في جانب الكائن.

2.3. النمط 3: استدعاء يعبر Apartment

بين شقق مختلفة يحوّل COM عبر Proxy/Stub. للواجهات القياسية يتولّى زمن تشغيل COM المعالجة.

في الجدول التالي تظهر كلمات خاصة بـ COM بلا شرح. نكتبها سطراً سطراً أوّلاً.

المصطلح المعنى
marshalling عند عبور حدود Apartment أو العملية، إعادة تعبئة الاستدعاء والوسائط إلى «شكل يمكن تمريره كما هو» ونقلها. تُعاد إلى الشكل الأصلي في الجانب الآخر من الحدود
Proxy / Stub زوج أجزاء يتولّى marshalling. من يقوم في جهة المستدعي هو Proxy (يستلم الاستدعاء متظاهراً بأنّه الأصل)، ومن يقوم في جهة المستدعى هو Stub (يمرّر الاستدعاء المستلم إلى الكائن الأصلي)
IDispatch واجهة COM تسأل عن اسم الدالة كنصّ وتستدعي برقم. لغات السكربت وVBA تستطيع استخدام COM بفضل هذه الآلية
Automation تسمية جامعة لاستخدام COM يفترض IDispatch وأنواع بيانات محدودة يمكن استخدامها هناك (BSTR وVARIANT وغيرها). إذا بقيت ضمن هذا النطاق، يتولّى oleaut32.dll في نظام التشغيل الـ marshalling
مكتبة الأنواع بيانات تكتب شكل الواجهة (الدوال وأنواع الوسائط) بشكل تقرؤه الآلة. تُوضع كملفّ .tlb وحده، أو مضمّنة في DLL / EXE
marshaller مكتبة الأنواع وظيفة قياسية في COM تقرأ مكتبة الأنواع وتجري marshalling في المكان. بفضلها تستغني عن صنع Proxy/Stub مخصّص
MIDL مترجم Microsoft يولّد شيفرة Proxy/Stub وغيرها من تعريف الواجهة (.idl)

انتباه: Proxy/Stub لا يُجهَّز تلقائياً لكلّ شيء، لكن في الميدان تغنيك الحالات التي لا تحتاج توليداً صريحاً في الغالب.

النمط تجهيز Proxy/Stub
مرتكز على IDispatch (Automation) غير لازم. يعالج oleaut32.dll
مكتبة أنواع مسجّلة غير لازم. يعالج marshaller مكتبة الأنواع
.NET COM Interop غير لازم عادة. يعمل عبر مكتبة الأنواع
واجهة مخصّصة مشتقّة مباشرة من IUnknown يلزم توليد Proxy/Stub بـ MIDL وتسجيله

أي أنّ توليد Proxy/Stub بـ MIDL يلزم عندما تصنع واجهة مشتقّة مباشرة من IUnknown دون استخدام IDispatch. في مكوّنات COM الشائعة المستخدمة من .NET أو لغات السكربت يقلّ أن تحتاج هذا العمل.

نقطة الانفصال: هل تجهّز Proxy/Stub بنفسكمخطّط يبيّن نقطة الانفصال: إذا كفى marshalling القياسي في COM مثل المرتكز على IDispatch أو marshaller مكتبة الأنواع فلا يلزم توليد Proxy/Stub، ولا يلزم التوليد والتسجيل بـ MIDL إلا لواجهة مخصّصة مشتقّة مباشرة من IUnknown لا يغطيها ذلك.نعملاهل يكفي marshalling القياسي؟توليد Proxy/Stub غير لازمتوليد Proxy/Stub بـ MIDL وتسجيلهيعالج IDispatch أو مكتبة الأنواعهذا الغالب في الميدان

الشكل 6: التوليد الصريح لـ Proxy/Stub يكون عندما تكون الواجهة مخصّصة مشتقّة مباشرة من IUnknown ولا يكفي marshalling القياسي.

خيط MTAزمن تشغيل COM (تلقائي)خيط STAاستدعاءتحويلكائن COMProxyRPC/IPCStubشيفرة المستدعي

الشكل 7: الاستدعاء الذي يعبر Apartment يُحوَّل عبر Proxy وRPC وStub التي يجهّزها زمن تشغيل COM تلقائياً.

النقطة الجوهرية: عبور Apartment يُحدث تكلفة marshalling. في الاستدعاء عالي التكرار يؤثّر في الأداء، فيلزم اعتباره عند التصميم.

2.4. مقياس تقريبي لتكلفة marshalling

فيما يلي مقياس عام (ليس قيمة مقيسة، ويتغيّر كثيراً بحسب الوضع وتعقيد الوسائط).

نمط الاستدعاء الزمن التقريبي الإحساس النسبي
داخل الشقّة نفسها (مباشر) 10 إلى 100 نانوثانية قريب من استدعاء دالة عادي
شقّة مختلفة (العملية نفسها) 1 إلى 10 ميكروثانية 100 إلى 1000 ضعف الاستدعاء المباشر
عملية مختلفة (Out-of-proc) 100 إلى 1000 ميكروثانية 10 آلاف إلى 100 ألف ضعف الاستدعاء المباشر

مقارنة نسبية:

  • الشقّة نفسها: بقدر وصول واحد إلى الذاكرة
  • شقّة مختلفة: بقدر استدعاء نظام واحد
  • عملية مختلفة: بقدر تواصل شبكي إلى المضيف المحلّي

في موضع يستدعي عشرة آلاف مرّة في حلقة يظهر هذا الفرق بقوّة.

كيف تتعامل مع هذه الأرقام

الجدول أعلاه للإمساك بإحساس الرتبة (الأُسّ)، وليس قيمة مقيسة لها مصدر. لا يوجد أيضاً مقياس موحّد منشور، فلا تستخدم الرقم نفسه مستنداً لحكم تصميم.

في المقابل، «داخل الشقّة نفسها استدعاء مباشر، وعند عبور الحدود يُقحم marshalling حتماً» بنية مكتوبة كمواصفة في وثائق Microsoft. في باب Single-Threaded Apartments منصوص أنّ مؤشّر الواجهة داخل الشقّة نفسها يمكن تمريره بلا marshalling، وأنّ عبور Apartment يستخدم آلية marshalling نفسها التي بين العمليات حتى داخل العملية نفسها، وأنّ الاستدعاء يصل كرسالة نافذة إلى نافذة مخفية (OleMainThreadWndClass). تغيّر الرتبة سببه هذه البنية، والرقم نفسه يتحرّك بحسب البيئة وتعقيد الوسائط.

إذا أردت الحكم في حالتك، فالأضمن أن تقيس الاستدعاء نفسه من الشقّة نفسها ومن شقّة أخرى وتقارن.

السبب البنيوي لتغيّر الرتبةمخطّط يبيّن السبب البنيوي لتغيّر الرتبة: داخل الشقّة نفسها استدعاء مباشر، أمّا عبور Apartment فيُقحم marshalling حتماً حتى داخل العملية نفسها، وإذا كانت الوجهة STA يصل الاستدعاء كرسالة نافذة إلى نافذة مخفية.استدعاء داخل الشقّة نفسهااستدعاء مباشراستدعاء يعبر Apartmentيُقحم marshalling حتماًوجهة STA تصل إلى نافذة مخفية

الشكل 8: تغيّر الرتبة سببه بنية المواصفة: عبور الحدود يُقحم marshalling حتماً.

// C#。同じ呼び出しを N 回繰り返して 1 回あたりの時間を出す
static void Measure(string label, Action call, int iterations = 100_000)
{
    call(); // 初回の遅延(JIT、プロキシ生成、接続確立)を計測から外す

    var sw = System.Diagnostics.Stopwatch.StartNew();
    for (int i = 0; i < iterations; i++)
    {
        call();
    }
    sw.Stop();

    double perCallNs = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
    Console.WriteLine($"{label}: {perCallNs:F1} ns/call");
}

إذا قستَ دالة قليلة الوسائط ودالة تمرّر سلسلة أو مصفوفة، ترى أيضاً أنّ «أثر كمية marshalling يتغيّر».

3. STA (Single-Threaded Apartment)

STA نموذج «خيط واحد = شقّة واحدة».

  • كائن COM داخل تلك الشقّة يُنفَّذ أساساً على ذلك الخيط فقط
  • الاستدعاء من خيط آخر يحوّله COM عبر طابور الرسائل/RPC
  • يُستخدم كثيراً في خيط واجهة المستخدم (WinForms/WPF) (واجهة المستخدم أيضاً «ألفة خيط واحد + حلقة رسائل» فالتوافق جيد)
نموذج تنفيذ STAمخطّط يبيّن أنّ كائن COM داخل شقّة STA يُنفَّذ أساساً على الخيط الذي أنشأه فقط، وأنّ الاستدعاء من خيط آخر يحوّله COM إلى ذلك الخيط عبر طابور الرسائل أو RPC.كائن COM من نوع STAيُنفَّذ على الخيط الذي أنشأه فقطاستدعاء من خيط آخرتحويل عبر طابور الرسائل / RPC

الشكل 9: كائن STA يعمل على خيط صاحبه فقط، والاستدعاء من الخارج يصل بعد تحويل.

3.1. لماذا يُستخدم STA في خيط واجهة المستخدم

لأنّ خيط واجهة المستخدم وSTA متطابقان في التصميم.

  • عناصر تحكّم واجهة المستخدم ليست thread-safe الأزرار وحقول النصّ وغيرها لا يمكن التعامل معها بأمان إلا من الخيط الذي أنشأها
  • STA أيضاً «ألفة خيط واحد» كائن COM يُنفَّذ مباشرة على الخيط الذي أنشأه فقط
  • خيط واجهة المستخدم يدير حلقة رسائل حتماً لازم لمعالجة أحداث النوافذ. يتطابق مع افتراض STA (مضخّة الرسائل)

لذلك خيط واجهة المستخدم في WinForms/WPF STA افتراضياً.

تطابق تصميم خيط واجهة المستخدم وSTAمخطّط يبيّن أنّ عناصر تحكّم واجهة المستخدم لا يمكن التعامل معها بأمان إلا من الخيط الذي أنشأها، وأنّ STA أيضاً بألفة خيط واحد، وأنّ حلقة الرسائل التي يديرها خيط واجهة المستخدم حتماً تتطابق مع مضخّة الرسائل التي يفترضها STA، لذلك يكون خيط واجهة المستخدم STA افتراضياً.ألفة خيط واحد لعناصر واجهة المستخدمالتصميم متطابقألفة خيط واحد في STAحلقة رسائل خيط واجهة المستخدمافتراض STA (مضخّة الرسائل)خيط واجهة المستخدم STA افتراضياً

الشكل 10: لأنّ التصميم يتطابق في نقطتين، ألفة خيط واحد وحلقة الرسائل، صار خيط واجهة المستخدم STA.

النقطة الجوهرية: STA ألفته للخيط عالية، وفي المقابل يزدحم بسهولة إذا كثر المستدعون.

4. MTA (Multi-Threaded Apartment)

MTA نموذج «شقّة واحدة لعدّة خيوط».

  • يُستدعى كائن COM في الوقت نفسه من عدّة خيوط
  • في جانب الكائن تصميم thread-safe إلزامي
  • يناسب معالجة جهة الخادم والمعالجة في الخلفية

النقطة الجوهرية: MTA توازيه عالٍ، لكن مسؤولية تنفيذ الكائن ثقيلة.

نموذج تنفيذ MTAمخطّط يبيّن أنّ MTA يشارك شقّة واحدة بين عدّة خيوط، وأنّ كائن COM يُستدعى في الوقت نفسه من عدّة خيوط، لذلك يلزم تصميم thread-safe في جانب الكائن.MTA (شقّة واحدة لعدّة خيوط)يُستدعى في الوقت نفسه من عدّة خيوطتصميم thread-safe في جانب الكائن إلزامييناسب جهة الخادم والمعالجة في الخلفية

الشكل 11: MTA يمكن استدعاؤه على التوازي، وفي المقابل تنتقل مسؤولية السلامة إلى تنفيذ الكائن.

5. أين يُحسم STA/MTA

شقّة COM تُحسم بتهيئة كلّ خيط.

  • في لحظة استدعاء CoInitialize / CoInitializeEx تُحسم شقّة ذلك الخيط
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED
لحظة حسم الشقّةمخطّط يبيّن أنّ شقّة الخيط تُحسم في لحظة استدعائه CoInitialize أو CoInitializeEx، فتصير STA إن كان COINIT_APARTMENTTHREADED، وMTA إن كان COINIT_MULTITHREADED.COINIT_APARTMENTTHREADEDCOINIT_MULTITHREADEDالخيطيستدعي CoInitialize / CoInitializeExتُحسم STAتُحسم MTA

الشكل 12: الشقّة تُحسم بتعيين لحظة استدعاء التهيئة لكلّ خيط.

5.1. STA/MTA في .NET

في .NET أيضاً صفات [STAThread] / [MTAThread] وApartmentState، لكنّها غلاف لضبط نموذج Apartment في COM.

  • [STAThread] ← تُوضع على دالة Main (نقطة الدخول). عند استخدام COM يُهيَّأ كـ STA
  • [MTAThread] ← بالمثل لدالة Main. يُهيَّأ كـ MTA
  • Thread.SetApartmentState(ApartmentState.STA) ← لخيوط إضافية تنشئها. يلزم الضبط قبل بدء الخيط

نقاط انتباه:

  • حتى بوجود [STAThread]، لا تُهيَّأ حتى يُستدعى COM فعلاً (لا أثر إن لم يُستخدم COM)
  • [STAThread] لا يؤثّر على خيوط إضافية. استخدم Thread.SetApartmentState

أي أنّ STA/MTA في .NET هما STA/MTA في COM نفسهما، وآلية مُعدَّة من أجل COM Interop.

مهم: لا يمكن تغيير الشقّة لاحقاً. التهيئة الأولى هي كلّ شيء.

أين تضبط الشقّة في .NETمخطّط يبيّن أنّ دالة Main تُعلَّم بصفة STAThread أو MTAThread، وأنّ الخيوط الإضافية تُضبط بـ Thread.SetApartmentState قبل البدء، وأنّ الشقّة في الحالتين تُحسم عند أوّل تهيئة ولا يمكن تغييرها لاحقاً.دالة Mainصفة [STAThread]/[MTAThread]خيط إضافي تنشئهThread.SetApartmentState قبل البدءالشقّة تُحسم عند أوّل تهيئةلا يمكن تغييرها لاحقاً

الشكل 13: نقطة الدخول بالصفات، والخيوط الإضافية بـ SetApartmentState، وفي الحالتين تُحسم من المرّة الأولى.

6. مثال ملموس على تعليق ينتج عن خطأ في STA

تركيبة كهذه يسهل أن تُحدث تعليقاً فعلاً.

6.1. وضع شائع

  • إنشاء خيط STA في الخلفية وتوليد كائن COM هناك
  • ذلك الخيط لا يدير حلقة رسائل
  • استدعاء ذلك الكائن من خيط آخر (سواء كان STA أو MTA)
تركيبة يسهل أن تُحدث تعليقاًمخطّط يبيّن تركيبة يسهل أن تُحدث تعليقاً: خيط STA أُنشئ في الخلفية ولّد كائن COM لكنّه لا يدير حلقة رسائل، ويستدعي خيط آخر ذلك الكائن.يستدعيخيط STA في الخلفيةيولّد كائن COMلا يدير حلقة رسائلخيط آخر (سواء STA / MTA)

الشكل 14: الجمع الخطِر هو «استدعاء كائن يملكه STA لا يدير حلقة، من خيط آخر».

6.2. ماذا يحدث

سبب التعليق يُختصر إلى افتراضين لـ STA. هذا القسم هو شرح السبب، والأقسام التالية لا تكرّره.

  • كائن COM يُعالَج على خيط STA الذي أنشأه سواء كان المستدعي STA أو MTA، الاستدعاء من خيط آخر يُحوَّل حتماً إلى خيط STA ذاك. التحويل يصل كرسالة نافذة إلى نافذة مخفية يصنعها COM لتلك الشقّة (صنف النافذة OleMainThreadWndClass)
  • لاستلام ذلك التحويل يلزم أن يدير خيط STA مضخّة رسائل وثائق Microsoft أيضاً تنصّ على أنّ «كلّ STA يجب أن يملك حلقة رسائل لمعالجة الاستدعاءات من عمليات أخرى أو من شقق أخرى داخل العملية نفسها»

لذلك خيط STA الذي لا يدير الرسائل لا يستلم الاستدعاء، فيبقى المستدعي ينتظر الردّ، والنتيجة تعليق.

التدفّق حتى التعليقمخطّط يبيّن التدفّق: الاستدعاء من خيط آخر يُحوَّل إلى خيط STA الذي أنشأ الكائن كرسالة نافذة إلى نافذة مخفية، لكن إن لم يدير خيط STA مضخّة رسائل لا يستلمه، فيبقى المستدعي ينتظر الردّ ويحدث تعليق.استدعاء من خيط آخرتحويل إلى خيط STA الذي أنشأيصل كرسالة إلى نافذة مخفيةمضخّة الرسائل لا تدورجانب STA لا يستلم الاستدعاءالمستدعي يبقى ينتظر الردّتعليق

الشكل 15: التحويل يصل كرسالة، لذلك في STA توقّفت مضخّته لا يبقى من يستلم.

وهذا نفسه في .NET. في وثائق Single-Threaded Apartments تحذير: إذا حُظر خيط STA بـ Task.Wait() أو Task.Result أو Thread.Sleep() أو ManualResetEvent.WaitOne() وغيرها، لا تكتمل ردود نداء COM ولا الاستدعاءات التي تعبر الشقّة فيحدث تعطّل. مثال الفشل في 6.3 التالي متوقّف عند WaitOne()، وهو هذا الشكل بالضبط.

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

6.3. شيفرة تقريبية (نمط الفشل النموذجي)

using System;
using System.Runtime.InteropServices;
using System.Threading;

internal static class StaHangDemo
{
    private const uint COINIT_APARTMENTTHREADED = 0x2;

    [DllImport("ole32.dll")]
    private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);

    [DllImport("ole32.dll")]
    private static extern void CoUninitialize();

    public static void Run(string progId)
    {
        var ready = new AutoResetEvent(false);
        var done = new AutoResetEvent(false);

        object comObj = null;

        var staThread = new Thread(() =>
        {
            // تهيئة كـ STA
            CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

            // يفترض صنف COM مسجّل بـ ThreadingModel=Apartment
            Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
            comObj = Activator.CreateInstance(type);
            ready.Set();

            // انتظار بلا حلقة رسائل -> هنا الضرر القاتل
            done.WaitOne();

            CoUninitialize();
        });

        staThread.SetApartmentState(ApartmentState.STA);
        staThread.Start();

        ready.WaitOne();

        try
        {
            // الاستدعاء من خيط آخر (STA أو MTA) يحوّل الاستدعاء إلى STA
            // لكن جانب STA لا يعالج الرسائل, فيسهل التعليق هنا
            dynamic obj = comObj;
            obj.AnyMethod();
        }
        finally
        {
            // حتى إن عاد الاستدعاء -- كان الصنف agile,
            // أو لم يحوّل الاستدعاء, أو عاد AnyMethod فورا --
            // حرّر خيط STA حتما. إن أسقطت هذا, يبقى خيط أمامي
            // ينتظر done, فلا تنتهي العملية حتى عند "عدم التكرار".
            // يتعذر تمييز وقوع التكرار من عدمه عبر العرض
            done.Set();
            staThread.Join();
        }
    }
}

هذه الشيفرة تفترض تمرير ProgID لصنف COM مسجّل كـ ThreadingModel=Apartment (= STA). استبدل AnyMethod باسم دالة يملكها ذلك الصنف فعلاً. لا يتكرّر مع أيّ صنف COM. شروط التكرار الثلاثة التالية.

الشرط طريقة التحقّق
ThreadingModel للصنف المستهدف هو Apartment انظر قيمة ThreadingModel في HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32 في السجلّ. إذا كان Both أو Free لا يُحوَّل الاستدعاء ولا يتكرّر
خيط STA لا يدير الرسائل done.WaitOne() في المثال أعلاه هو ذلك
الاستدعاء من خيط آخر ما دام الاستدعاء داخل خيط STA نفسه فهو مباشر، فلا يحدث تعليق

سبب إحاطة obj.AnyMethod() بـ try / finally وتمرير done.Set() وstaThread.Join() حتماً هو تمييز «حالة عدم التكرار» هذه. خيط STA افتراضياً خيط أمامي، فما لم يُرفع done لا تنتهي العملية. إذا حذفتَ finally، يصير العرض الخارجي في حالة التكرار (الاستدعاء لا يعود) وحالة عدم التكرار (عاد الاستدعاء لكن لا أحد يرفع done) معاً «العملية لا تنتهي». أي أنّ أداة التحقّق من شروط التكرار تخفي نجاح الشرط أو فشله. بالشكل أعلاه، إذا لم يتكرّر ينتهي البرنامج انتهاءً سليماً.

للتحقّق من التوقّف، الأسرع إيقاف العملية مؤقتاً في المنقّح ورؤية أنّ مكدّس خيط المستدعي متوقّف عند انتظار COM، وأنّ خيط STA متوقّف عند WaitOne.

زمن تشغيل COMخيط STAالخيط الرئيسزمن تشغيل COMخيط STAالخيط الرئيسبلا حلقة رسائلعالِق هنايحوّل برسالة لكن...أثناء WaitOneلا يعالج الرسائلالمستدعي أيضاً يبقى منتظراًالطرفان في انتظار → تعليقبدء الخيطCoInitializeEx (STA)توليد كائن COMready.Set()انتظار عند done.WaitOne()CallComObject()يحاول تحويل الاستدعاء

الشكل 16: خيط STA المتوقّف عند WaitOne وخيط رئيس ينتظر ردّ التحويل يعجزان عن التقدّم معاً.

السطران في وسط الرسم، «يحوّل برسالة لكن…»، هما موضع انهيار الافتراضين المكتوبين في 6.2.

6.4. نقاط التجنّب

  • إذا استقبلت استدعاءات من خيوط أخرى، يلزم أن يدير خيط STA حلقة رسائل
  • إن أمكن ولِّد واستخدم على خيط واجهة المستخدم (خيط واجهة المستخدم يملك حلقة رسائل من البداية)
  • إن لم تكن تحتاج STA ابدأ بـ MTA من البداية

تذييل: إذا اكتمل كلّ شيء داخل الخيط نفسه، ليس Application.Run() لازماً دائماً. غير أنّ أنظمة واجهة المستخدم وCOM كثيراً ما يدخل فيها استدعاء من خيط آخر، لذلك في الميدان يكاد يكون إلزامياً.

ثلاثة اتجاهات لتجنّب التعليقمخطّط يبيّن ثلاثة اتجاهات للتجنّب: إدارة حلقة رسائل على خيط STA إذا استقبل استدعاءات من خيوط أخرى، أو التوليد والاستخدام على خيط واجهة المستخدم الذي يملك حلقة رسائل من البداية إن أمكن، أو البدء بـ MTA من البداية إن لم تكن تحتاج STA.كيف تتجنّب تعليق STAأدر حلقة رسائل على خيط STAولِّد واستخدم على خيط واجهة المستخدمإن لم تكن تحتاج STA فابدأ بـ MTAخيط واجهة المستخدم يملك الحلقة من البداية

الشكل 17: وسائل التجنّب تُرتَّب في ثلاثة اتجاهات: إدارة الحلقة، أو الانتقال إلى موضع يملك حلقة، أو ترك STA.

6.5. ماذا يعني في النهاية «تشغيل حلقة الرسائل»؟

هذا الذي يفعله خيط واجهة المستخدم في Win32.

while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

في STA تصل الاستدعاءات من خيوط أخرى «محوَّلة». هذه الحلقة (مضخّة الرسائل) هي ما يستلم ذلك التحويل ويمرّره إلى التنفيذ.

دور مضخّة الرسائلمخطّط يبيّن أنّ مضخّة الرسائل هي هذا التكرار: استلام استدعاء حُوِّل من خيط آخر بـ GetMessage، وتمريره إلى التنفيذ بـ DispatchMessage.استدعاء حُوِّلالاستلام بـ GetMessageالتمرير إلى التنفيذ بـ DispatchMessage

الشكل 18: «تشغيل حلقة الرسائل» هو هذا التكرار: استلام التحويل وتمريره إلى التنفيذ.

6.6. مثال في الاتجاه الصحيح (تقريباً)

إذا أردت استخدام COM على STA في الخلفية، يصير الشكل كالتالي.

var ready = new AutoResetEvent(false);
object comObj = null;

var staThread = new Thread(() =>
{
    CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

    comObj = new SomeStaComObject();
    ready.Set();

    // STAスレッドが生きている間はメッセージを回す
    Application.Run();

    CoUninitialize();
});

staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();

ready.WaitOne();
CallComObject(comObj);

(※ نسيان استدعاء CoInitializeEx / CoUninitialize حادث شائع. يلزم إعلان P/Invoke لـ CoInitializeEx نفسه الذي في 6.3)

شكل Application.Run() بلا وسائط مخصّص لإدارة حلقة الرسائل فقط دون نموذج. هذا الموضع بالضبط هو الاستخدام المتصوَّر، لكن امسك بالنقطتين التاليتين.

  • يلزم مرجع إلى System.Windows.Forms. في تطبيق وحدة تحكّم أضف <UseWindowsForms>true</UseWindowsForms> إلى ملفّ المشروع
  • هذه الحلقة لا تنتهي بنفسها. لإيقافها استدعِ على ذلك الخيط Application.ExitThread() (أو Application.Exit() لإنهاء التطبيق كلّه). إذا أردت الوصول إلى CoUninitialize() في المثال أعلاه، يلزم آلية منفصلة تنقل «إنهاء» إلى خيط STA وتستدعي ExitThread

إذا لم ترد الاعتماد على WinForms، اكتب حلقة GetMessage / DispatchMessage في 6.5 بنفسك، أو استخدم MsgWaitForMultipleObjects الذي ينتظر الرسائل وكائن المزامنة معاً. الثاني نمط شائع عندما تريد «استلام إشعار إنهاء بحدث دون إسقاط استدعاءات COM».

ثلاث وسائل لإدارة حلقة على STA في الخلفيةمخطّط يبيّن أنّ لإدارة حلقة رسائل على STA في الخلفية ثلاث وسائل: استخدام Application.Run الذي يحتاج مرجع WinForms، أو كتابة حلقة GetMessage وDispatchMessage بنفسك، أو استخدام MsgWaitForMultipleObjects الذي ينتظر الرسائل وكائن المزامنة معاً.إدارة حلقة على STA في الخلفيةApplication.Runكتابة حلقة GetMessage بنفسكMsgWaitForMultipleObjectsيلزم مرجع WinForms ووسيلة إنهاءينتظر الرسائل وكائن المزامنة معاً

الشكل 19: طرق إدارة الحلقة ثلاث، وتُختار بحسب طريقة الإنهاء وثقل الاعتماد.

6.7. مثال تعليق آخر: ردّ نداء أثناء استدعاء متزامن

STA ليس «تحويل الاستدعاء» فقط؛ بحسب الوضع يأتي ردّ نداء في الاتجاه المعاكس (خادم ← عميل). ومن بين ذلك نمط وقوع ردّ نداء أثناء استدعاء متزامن نمط تعطّل كلاسيكي.

خادم COMخيط واجهة المستخدم (STA)خادم COMخيط واجهة المستخدم (STA)ينتظر عودة DoWork(لا يعالج الرسائل)أثناء الانتظارلا يستلم ردّ النداءينتظر اكتمال ردّ النداءكلّ طرف ينتظر الآخر → تعطّلDoWork() (استدعاء متزامن)ProgressCallback() (ردّ نداء)

الشكل 20: خيط واجهة مستخدم ينتظر عودة استدعاء متزامن، وخادم ينتظر اكتمال ردّ نداء، ينتظران بعضهما.

لماذا يسهل أن يصير تعطّلاً:

  1. خيط واجهة المستخدم يستدعي DoWork() استدعاءً متزامناً (حظر)
  2. خيط واجهة المستخدم ينتظر العودة (لا يعالج الرسائل)
  3. الخادم يرسل ProgressCallback() إلى خيط واجهة المستخدم
  4. خيط واجهة المستخدم أثناء الانتظار لا يستلم ردّ النداء
  5. الخادم ينتظر اكتمال ردّ النداء
  6. كلّ طرف ينتظر الآخر → لا يتقدّم أبداً

طول زمن المعالجة غير ذي صلة. نمط «وصول ردّ نداء أثناء استدعاء متزامن» نفسه يسهل أن يصير مشكلة.

تذييل: في COM بحسب الوضع آلية تدير الرسائل أو تعيد الدخول، ويتغيّر السلوك بحسب المكوّن وشكل الاستدعاء. ليس حتماً أن يحدث تعطّل، لكن تجنّب هذا النمط أسلم.

7. تقسيم استخدام تقريبي

الوضع ما تختاره سبب الحكم ما يلزم معه
تدخل واجهة مستخدم (WinForms / WPF) STA عناصر تحكّم واجهة المستخدم بألفة خيط واحد، وخيط واجهة المستخدم يملك حلقة رسائل من الأصل (3.1) لا شيء خاص. STA افتراضياً
تريد معالجة متوازية كثيفة MTA عدّة خيوط تشارك شقّة واحدة ويمكن الاستدعاء مباشرة بلا تحويل (2.2) تصميم thread-safe في جانب كائن COM. ليست حصراً في جهة المستدعي، بل مسؤولية تنفيذ الكائن
تريد استخدام COM من نوع STA في الخلفية STA + حلقة رسائل ما دام يُستدعى من خيط آخر، يلزم فم يستلم التحويل (6.2) حلقة Application.Run() أو GetMessage. صمّم أيضاً وسيلة الإنهاء (ExitThread وغيرها) معاً (6.6)
متطلّب مكوّن COM المستخدم محسوم اتبع الطرف الآخر الشقّة هي قواعد الاستدعاء نفسها، ولا يمكن تغييرها لاحقاً (الباب 5) تحقّق من قيمة ThreadingModel. إذا كان Apartment ركّب بافتراض STA
استدعاء عالي التكرار قرّب إلى الشقّة نفسها مع جهة الاستدعاء كلّ عبور للحدود يُقحم marshalling (2.4) إن صعب، صمّم لتقليل عدد الاستدعاءات نفسه (التمرير مجمّعاً)

أولوية الحسم عند الحيرة أسهل بهذا الترتيب: «متطلّب الطرف الآخر > وجود واجهة مستخدم > التوازي». الشقّة تُحسم عند أوّل تهيئة ولا يمكن تغييرها لاحقاً، لذلك احسم هذا فقط قبل بدء التنفيذ.

الترتيب الذي تنظر إليه عند الحيرةمخطّط يبيّن أنّه عند الحيرة في تقسيم استخدام STA وMTA يسهل الحسم بالتحقّق بهذا الترتيب: متطلّب مكوّن COM المستخدم، ثمّ وجود واجهة مستخدم، ثمّ التوازي.ثمّثمّمتطلّب الطرف الآخروجود واجهة مستخدمالتوازي

الشكل 21: إذا حرت في التقسيم، تحقّق بترتيب متطلّب الطرف الآخر، ثمّ وجود واجهة مستخدم، ثمّ التوازي.

8. خلاصة

STA/MTA نموذج خيوط من أجل COM، وSTA شكل خيط واحد = شقّة واحدة، وMTA شكل شقّة واحدة لعدّة خيوط. الاستدعاء الذي يعبر Apartment يحوّله COM عبر Proxy/Stub (للواجهات غير القياسية يلزم التوليد والتسجيل بـ MIDL وغيرها)، لكنّ ذلك يصاحبه تكلفة marshalling، لذلك في المواضع المتوقّع فيها استدعاء عالي التكرار تريد حسم تصميم الشقّة بحذر.

من زاوية التعليق، الأمر يُختصر إلى نقطة واحدة: «خيط STA الذي يستقبل استدعاءات من خيوط أخرى يفترض إدارة مضخّة رسائل». الاستدعاء إلى خيط STA لا يدير الرسائل يسهل أن يعلّق، ونمط وصول ردّ نداء أثناء استدعاء متزامن يسهل أن يصير تعطّلاً. خيط واجهة المستخدم يملك «ألفة خيط واحد» و«حلقة رسائل» من البداية، فيلبّي هذا الافتراض دون تنفيذ إضافي، لذلك توافقه مع COM من نوع STA جيد.

9. مراجع

  • نموذج Apartment https://learn.microsoft.com/en-us/windows/win32/com/com-apartments
  • CoInitializeEx https://learn.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitializeex
  • Single-Threaded Apartments (سبب إلزام حلقة الرسائل، النافذة المخفية، تحذير التعطّل في .NET) https://learn.microsoft.com/en-us/windows/win32/com/single-threaded-apartments
  • Multithreaded Apartments https://learn.microsoft.com/en-us/windows/win32/com/multithreaded-apartments
  • InprocServer32 (قيمة ThreadingModel) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32
  • دالة Application.Run (شكل إدارة حلقة الرسائل بلا نموذج، وطريقة إيقافها) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
  • MsgWaitForMultipleObjects (انتظار الرسائل وكائن المزامنة معاً) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects

تنزيل ملفّ Word لهذا المقال

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

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

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

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

أيّهما تختار، STA أم MTA؟
الأساس: STA إذا دخلت واجهة مستخدم في المعالجة، وMTA إذا أردت معالجة متوازية كثيفة. STA شكل شقّة واحدة لكلّ خيط وألفته للخيط عالية، لكنّه يزدحم بسهولة إذا كثر المستدعون. MTA يشارك شقّة واحدة بين عدّة خيوط فالتوازي أعلى، لكن يلزم تصميم thread-safe في جانب كائن COM. إن لم يرجّح أيّ منهما، فالواقع أن تتبع متطلّب المكتبة القائمة أو خادم COM الذي تستخدمه.
لماذا خيط واجهة المستخدم STA؟
لأنّ تصميم خيط واجهة المستخدم وSTA متطابقان. عناصر التحكّم في واجهة المستخدم مثل الأزرار وحقول النصّ ليست thread-safe، ولا يمكن التعامل معها بأمان إلا من الخيط الذي أنشأها. STA أيضاً نموذج بألفة خيط واحد. ثمّ إنّ خيط واجهة المستخدم يدير حلقة رسائل حتماً لمعالجة أحداث النوافذ، فيلبّي مضخّة الرسائل التي يفترضها STA دون تنفيذ إضافي. لذلك خيط واجهة المستخدم في WinForms/WPF STA افتراضياً.
لماذا قد يعلّق البرنامج عند استدعاء كائن COM من نوع STA؟
استدعاء كائن COM من نوع STA يُعالَج على خيط STA الذي أنشأه. الاستدعاء من خيط آخر يحوّله COM عبر رسالة/RPC، لكن إن لم يُدر خيط STA حلقة رسائل لا يستلم التحويل، فيبقى المستدعي منتظراً ويحدث تعليق. للتجنّب: أدر حلقة رسائل على خيط STA الذي يُستدعى من خيوط أخرى، أو أنشئ واستخدم على خيط واجهة المستخدم، أو ابدأ بـ MTA من البداية إن لم تكن تحتاج STA.
لماذا توجد صفة [STAThread] في .NET؟
هي غلاف لضبط نموذج Apartment في COM. إذا وُضعت على دالة Main، يُهيَّأ ذلك الخيط كـ STA عند استخدام COM. غير أنّ التهيئة لا تتم حتى يُستدعى COM فعلاً، فلا أثر لها في تطبيق لا يستخدم COM. كذلك لا تؤثّر على خيوط إضافية تنشئها، فاضبط Thread.SetApartmentState قبل بدء الخيط. الشقّة تُحسم عند أوّل تهيئة ولا يمكن تغييرها لاحقاً.

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

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

غو كومورا

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

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

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