أساسيات 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، ولماذا يحدث تعليق.
المحتويات
- 1. الخلاصة أوّلاً (بجملة)
- 2. أنماط الاستدعاء في نموذج Apartment (رسوم)
- 3. STA (Single-Threaded Apartment)
- 4. MTA (Multi-Threaded Apartment)
- 5. أين يُحسم STA/MTA
- 6. مثال ملموس على تعليق ينتج عن خطأ في STA
- 7. تقسيم استخدام تقريبي
- 8. خلاصة
- 9. مراجع
عند استخدام COM لا مفرّ من «على أيّ خيط يعمل». في صميم ذلك نموذج Apartment (STA/MTA). STA/MTA ليسا مفهوم خيوط Windows العام، بل نموذج خيوط لتحديد قواعد استدعاء كائنات COM.
يربط هذا المقال علاقة STA وMTA وCOM بالرسوم، ويصل حتى «لماذا قد يحدث تعليق».
flowchart TB
accTitle: موضع نموذج Apartment
accDescr: مخطّط يبيّن أنّ STA/MTA ليسا مفهوم خيوط Windows العام، بل الشكلان لنموذج Apartment، وهو نموذج خيوط لتحديد قواعد استدعاء كائنات COM.
com["قواعد استدعاء كائن COM"] --> am["نموذج Apartment"]
am --> sta["STA"]
am --> mta["MTA"]
am -.-> note["ليس مفهوم خيوط Windows العام"]
الشكل 1: STA/MTA هما الشكلان لنموذج Apartment الذي يحدّد قواعد استدعاء كائنات COM.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 20، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
1. الخلاصة أوّلاً (بجملة)
- قواعد استدعاء كائن COM تُحسم بحسب «أيّ Apartment ينتمي إليه»
- يسهل الفهم إذا رأيت STA شقّة واحدة لكلّ خيط، وMTA شقّة واحدة لعدّة خيوط
- الاستدعاء الذي يعبر Apartment يجري له COM marshalling عبر Proxy/Stub
flowchart TB
accTitle: علاقة STA وMTA وعبور Apartment
accDescr: مخطّط يبيّن الخلاصة: STA شكل شقّة واحدة لكلّ خيط، وMTA شكل شقّة واحدة لعدّة خيوط، والاستدعاء الذي يعبر Apartment يجري له COM marshalling عبر Proxy/Stub.
sta["STA (شقّة واحدة لكلّ خيط)"] -->|"استدعاء يعبر Apartment"| m["marshalling عبر Proxy / Stub"]
mta["MTA (شقّة واحدة لعدّة خيوط)"] -->|"استدعاء يعبر Apartment"| m
الشكل 2: الشقّة التي ينتمي إليها الكائن تحدّد قواعد الاستدعاء، ولا يُقحم marshalling إلا عند عبور Apartment.
2. أنماط الاستدعاء في نموذج Apartment (رسوم)
لاستدعاء كائن COM ثلاثة أنماط رئيسة.
flowchart TB
accTitle: الأنماط الثلاثة للاستدعاء
accDescr: مخطّط يبيّن أنّ لاستدعاء كائن COM ثلاثة أنماط: الاستدعاء داخل خيط STA نفسه، والاستدعاء داخل MTA نفسه، والاستدعاء الذي يعبر Apartment.
caller["استدعاء كائن COM"] --> p1["النمط 1: داخل خيط STA نفسه"]
caller --> p2["النمط 2: داخل MTA نفسه"]
caller --> p3["النمط 3: عبور Apartment"]
الشكل 3: تنقسم أنماط الاستدعاء إلى ثلاثة، وبحسب أيّها ينطبق تتغيّر التكلفة ونقاط الانتباه.
2.1. النمط 1: الاستدعاء داخل خيط STA نفسه
داخل خيط STA نفسه يمكن الاستدعاء مباشرة. بلا تكلفة إضافية.
flowchart LR
subgraph STA[خيط STA]
Caller[شيفرة المستدعي]
Obj[كائن COM]
Caller -->|استدعاء مباشر| Obj
end
الشكل 4: الاستدعاء داخل خيط STA نفسه استدعاء مباشر، بلا تكلفة إضافية.
2.2. النمط 2: الاستدعاء داخل MTA نفسه
من عدّة خيوط داخل MTA يمكن الاستدعاء مباشرة من أيّ خيط. غير أنّ جانب الكائن يحتاج تصميم thread-safe إلزاماً.
flowchart LR
subgraph MTA["MTA (شقّة واحدة)"]
Thread1[خيط عامل 1]
Thread2[خيط عامل 2]
Obj[كائن COM]
Thread1 -->|استدعاء مباشر| Obj
Thread2 -->|استدعاء مباشر| Obj
end
الشكل 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 أو لغات السكربت يقلّ أن تحتاج هذا العمل.
flowchart TB
accTitle: نقطة الانفصال: هل تجهّز Proxy/Stub بنفسك
accDescr: مخطّط يبيّن نقطة الانفصال: إذا كفى marshalling القياسي في COM مثل المرتكز على IDispatch أو marshaller مكتبة الأنواع فلا يلزم توليد Proxy/Stub، ولا يلزم التوليد والتسجيل بـ MIDL إلا لواجهة مخصّصة مشتقّة مباشرة من IUnknown لا يغطيها ذلك.
q{"هل يكفي marshalling القياسي؟"} -->|"نعم"| auto["توليد Proxy/Stub غير لازم"]
q -->|"لا"| midl["توليد Proxy/Stub بـ MIDL وتسجيله"]
auto -.-> how["يعالج IDispatch أو مكتبة الأنواع"]
auto -.-> few["هذا الغالب في الميدان"]
الشكل 6: التوليد الصريح لـ Proxy/Stub يكون عندما تكون الواجهة مخصّصة مشتقّة مباشرة من IUnknown ولا يكفي marshalling القياسي.
flowchart LR
subgraph STA[خيط STA]
StaCaller[شيفرة المستدعي]
end
subgraph RT["زمن تشغيل COM (تلقائي)"]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[خيط MTA]
MtaObj[كائن COM]
end
StaCaller -->|استدعاء| Proxy
Stub -->|تحويل| MtaObj
الشكل 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). تغيّر الرتبة سببه هذه البنية، والرقم نفسه يتحرّك بحسب البيئة وتعقيد الوسائط.
إذا أردت الحكم في حالتك، فالأضمن أن تقيس الاستدعاء نفسه من الشقّة نفسها ومن شقّة أخرى وتقارن.
flowchart TB
accTitle: السبب البنيوي لتغيّر الرتبة
accDescr: مخطّط يبيّن السبب البنيوي لتغيّر الرتبة: داخل الشقّة نفسها استدعاء مباشر، أمّا عبور Apartment فيُقحم marshalling حتماً حتى داخل العملية نفسها، وإذا كانت الوجهة STA يصل الاستدعاء كرسالة نافذة إلى نافذة مخفية.
same["استدعاء داخل الشقّة نفسها"] --> direct["استدعاء مباشر"]
cross["استدعاء يعبر Apartment"] --> marshal["يُقحم marshalling حتماً"]
marshal -.-> msg["وجهة 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) (واجهة المستخدم أيضاً «ألفة خيط واحد + حلقة رسائل» فالتوافق جيد)
flowchart TB
accTitle: نموذج تنفيذ STA
accDescr: مخطّط يبيّن أنّ كائن COM داخل شقّة STA يُنفَّذ أساساً على الخيط الذي أنشأه فقط، وأنّ الاستدعاء من خيط آخر يحوّله COM إلى ذلك الخيط عبر طابور الرسائل أو RPC.
obj["كائن COM من نوع STA"] --> own["يُنفَّذ على الخيط الذي أنشأه فقط"]
other["استدعاء من خيط آخر"] --> fwd["تحويل عبر طابور الرسائل / RPC"]
fwd --> own
الشكل 9: كائن STA يعمل على خيط صاحبه فقط، والاستدعاء من الخارج يصل بعد تحويل.
3.1. لماذا يُستخدم STA في خيط واجهة المستخدم
لأنّ خيط واجهة المستخدم وSTA متطابقان في التصميم.
- عناصر تحكّم واجهة المستخدم ليست thread-safe الأزرار وحقول النصّ وغيرها لا يمكن التعامل معها بأمان إلا من الخيط الذي أنشأها
- STA أيضاً «ألفة خيط واحد» كائن COM يُنفَّذ مباشرة على الخيط الذي أنشأه فقط
- خيط واجهة المستخدم يدير حلقة رسائل حتماً لازم لمعالجة أحداث النوافذ. يتطابق مع افتراض STA (مضخّة الرسائل)
لذلك خيط واجهة المستخدم في WinForms/WPF STA افتراضياً.
flowchart TB
accTitle: تطابق تصميم خيط واجهة المستخدم وSTA
accDescr: مخطّط يبيّن أنّ عناصر تحكّم واجهة المستخدم لا يمكن التعامل معها بأمان إلا من الخيط الذي أنشأها، وأنّ STA أيضاً بألفة خيط واحد، وأنّ حلقة الرسائل التي يديرها خيط واجهة المستخدم حتماً تتطابق مع مضخّة الرسائل التي يفترضها STA، لذلك يكون خيط واجهة المستخدم STA افتراضياً.
ui["ألفة خيط واحد لعناصر واجهة المستخدم"] --> match["التصميم متطابق"]
sta["ألفة خيط واحد في STA"] --> match
loop["حلقة رسائل خيط واجهة المستخدم"] --> pre["افتراض STA (مضخّة الرسائل)"]
pre --> match
match --> def["خيط واجهة المستخدم STA افتراضياً"]
الشكل 10: لأنّ التصميم يتطابق في نقطتين، ألفة خيط واحد وحلقة الرسائل، صار خيط واجهة المستخدم STA.
النقطة الجوهرية: STA ألفته للخيط عالية، وفي المقابل يزدحم بسهولة إذا كثر المستدعون.
4. MTA (Multi-Threaded Apartment)
MTA نموذج «شقّة واحدة لعدّة خيوط».
- يُستدعى كائن COM في الوقت نفسه من عدّة خيوط
- في جانب الكائن تصميم thread-safe إلزامي
- يناسب معالجة جهة الخادم والمعالجة في الخلفية
النقطة الجوهرية: MTA توازيه عالٍ، لكن مسؤولية تنفيذ الكائن ثقيلة.
flowchart TB
accTitle: نموذج تنفيذ MTA
accDescr: مخطّط يبيّن أنّ MTA يشارك شقّة واحدة بين عدّة خيوط، وأنّ كائن COM يُستدعى في الوقت نفسه من عدّة خيوط، لذلك يلزم تصميم thread-safe في جانب الكائن.
mta["MTA (شقّة واحدة لعدّة خيوط)"] --> par["يُستدعى في الوقت نفسه من عدّة خيوط"]
par --> safe["تصميم thread-safe في جانب الكائن إلزامي"]
mta -.-> use["يناسب جهة الخادم والمعالجة في الخلفية"]
الشكل 11: MTA يمكن استدعاؤه على التوازي، وفي المقابل تنتقل مسؤولية السلامة إلى تنفيذ الكائن.
5. أين يُحسم STA/MTA
شقّة COM تُحسم بتهيئة كلّ خيط.
- في لحظة استدعاء
CoInitialize/CoInitializeExتُحسم شقّة ذلك الخيط - STA:
COINIT_APARTMENTTHREADED - MTA:
COINIT_MULTITHREADED
flowchart TB
accTitle: لحظة حسم الشقّة
accDescr: مخطّط يبيّن أنّ شقّة الخيط تُحسم في لحظة استدعائه CoInitialize أو CoInitializeEx، فتصير STA إن كان COINIT_APARTMENTTHREADED، وMTA إن كان COINIT_MULTITHREADED.
th["الخيط"] --> init["يستدعي CoInitialize / CoInitializeEx"]
init -->|"COINIT_APARTMENTTHREADED"| sta["تُحسم STA"]
init -->|"COINIT_MULTITHREADED"| mta["تُحسم MTA"]
الشكل 12: الشقّة تُحسم بتعيين لحظة استدعاء التهيئة لكلّ خيط.
5.1. STA/MTA في .NET
في .NET أيضاً صفات [STAThread] / [MTAThread] وApartmentState، لكنّها غلاف لضبط نموذج Apartment في COM.
[STAThread]← تُوضع على دالة Main (نقطة الدخول). عند استخدام COM يُهيَّأ كـ STA[MTAThread]← بالمثل لدالة Main. يُهيَّأ كـ MTAThread.SetApartmentState(ApartmentState.STA)← لخيوط إضافية تنشئها. يلزم الضبط قبل بدء الخيط
نقاط انتباه:
- حتى بوجود
[STAThread]، لا تُهيَّأ حتى يُستدعى COM فعلاً (لا أثر إن لم يُستخدم COM) [STAThread]لا يؤثّر على خيوط إضافية. استخدمThread.SetApartmentState
أي أنّ STA/MTA في .NET هما STA/MTA في COM نفسهما، وآلية مُعدَّة من أجل COM Interop.
مهم: لا يمكن تغيير الشقّة لاحقاً. التهيئة الأولى هي كلّ شيء.
flowchart TB
accTitle: أين تضبط الشقّة في .NET
accDescr: مخطّط يبيّن أنّ دالة Main تُعلَّم بصفة STAThread أو MTAThread، وأنّ الخيوط الإضافية تُضبط بـ Thread.SetApartmentState قبل البدء، وأنّ الشقّة في الحالتين تُحسم عند أوّل تهيئة ولا يمكن تغييرها لاحقاً.
main["دالة Main"] --> attr["صفة [STAThread]/[MTAThread]"]
newth["خيط إضافي تنشئه"] --> setap["Thread.SetApartmentState قبل البدء"]
attr --> fixed["الشقّة تُحسم عند أوّل تهيئة"]
setap --> fixed
fixed -.-> nochange["لا يمكن تغييرها لاحقاً"]
الشكل 13: نقطة الدخول بالصفات، والخيوط الإضافية بـ SetApartmentState، وفي الحالتين تُحسم من المرّة الأولى.
6. مثال ملموس على تعليق ينتج عن خطأ في STA
تركيبة كهذه يسهل أن تُحدث تعليقاً فعلاً.
6.1. وضع شائع
- إنشاء خيط STA في الخلفية وتوليد كائن COM هناك
- ذلك الخيط لا يدير حلقة رسائل
- استدعاء ذلك الكائن من خيط آخر (سواء كان STA أو MTA)
flowchart TB
accTitle: تركيبة يسهل أن تُحدث تعليقاً
accDescr: مخطّط يبيّن تركيبة يسهل أن تُحدث تعليقاً: خيط STA أُنشئ في الخلفية ولّد كائن COM لكنّه لا يدير حلقة رسائل، ويستدعي خيط آخر ذلك الكائن.
bg["خيط STA في الخلفية"] --> gen["يولّد كائن COM"]
bg --> noloop["لا يدير حلقة رسائل"]
other["خيط آخر (سواء STA / MTA)"] -->|"يستدعي"| gen
الشكل 14: الجمع الخطِر هو «استدعاء كائن يملكه STA لا يدير حلقة، من خيط آخر».
6.2. ماذا يحدث
سبب التعليق يُختصر إلى افتراضين لـ STA. هذا القسم هو شرح السبب، والأقسام التالية لا تكرّره.
- كائن COM يُعالَج على خيط STA الذي أنشأه
سواء كان المستدعي STA أو MTA، الاستدعاء من خيط آخر يُحوَّل حتماً إلى خيط STA ذاك. التحويل يصل كرسالة نافذة إلى نافذة مخفية يصنعها COM لتلك الشقّة (صنف النافذة
OleMainThreadWndClass) - لاستلام ذلك التحويل يلزم أن يدير خيط STA مضخّة رسائل وثائق Microsoft أيضاً تنصّ على أنّ «كلّ STA يجب أن يملك حلقة رسائل لمعالجة الاستدعاءات من عمليات أخرى أو من شقق أخرى داخل العملية نفسها»
لذلك خيط STA الذي لا يدير الرسائل لا يستلم الاستدعاء، فيبقى المستدعي ينتظر الردّ، والنتيجة تعليق.
flowchart TB
accTitle: التدفّق حتى التعليق
accDescr: مخطّط يبيّن التدفّق: الاستدعاء من خيط آخر يُحوَّل إلى خيط STA الذي أنشأ الكائن كرسالة نافذة إلى نافذة مخفية، لكن إن لم يدير خيط STA مضخّة رسائل لا يستلمه، فيبقى المستدعي ينتظر الردّ ويحدث تعليق.
caller["استدعاء من خيط آخر"] --> fwd["تحويل إلى خيط STA الذي أنشأ"]
fwd -.-> hidden["يصل كرسالة إلى نافذة مخفية"]
fwd --> nopump["مضخّة الرسائل لا تدور"]
nopump --> norecv["جانب STA لا يستلم الاستدعاء"]
norecv --> wait["المستدعي يبقى ينتظر الردّ"]
wait --> hang["تعليق"]
الشكل 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.
sequenceDiagram
participant Main as الخيط الرئيس
participant STA as خيط STA
participant COM as زمن تشغيل COM
Main->>STA: بدء الخيط
STA->>STA: CoInitializeEx (STA)
STA->>STA: توليد كائن COM
STA->>Main: ready.Set()
STA->>STA: انتظار عند done.WaitOne()
Note over STA: بلا حلقة رسائل<br/>عالِق هنا
Main->>COM: CallComObject()
COM->>STA: يحاول تحويل الاستدعاء
Note over COM: يحوّل برسالة لكن...
Note over STA: أثناء WaitOne<br/>لا يعالج الرسائل
Note over Main: المستدعي أيضاً يبقى منتظراً
Note over Main,STA: الطرفان في انتظار → تعليق
الشكل 16: خيط STA المتوقّف عند WaitOne وخيط رئيس ينتظر ردّ التحويل يعجزان عن التقدّم معاً.
السطران في وسط الرسم، «يحوّل برسالة لكن…»، هما موضع انهيار الافتراضين المكتوبين في 6.2.
6.4. نقاط التجنّب
- إذا استقبلت استدعاءات من خيوط أخرى، يلزم أن يدير خيط STA حلقة رسائل
- إن أمكن ولِّد واستخدم على خيط واجهة المستخدم (خيط واجهة المستخدم يملك حلقة رسائل من البداية)
- إن لم تكن تحتاج STA ابدأ بـ MTA من البداية
تذييل: إذا اكتمل كلّ شيء داخل الخيط نفسه، ليس Application.Run() لازماً دائماً.
غير أنّ أنظمة واجهة المستخدم وCOM كثيراً ما يدخل فيها استدعاء من خيط آخر، لذلك في الميدان يكاد يكون إلزامياً.
flowchart TB
accTitle: ثلاثة اتجاهات لتجنّب التعليق
accDescr: مخطّط يبيّن ثلاثة اتجاهات للتجنّب: إدارة حلقة رسائل على خيط STA إذا استقبل استدعاءات من خيوط أخرى، أو التوليد والاستخدام على خيط واجهة المستخدم الذي يملك حلقة رسائل من البداية إن أمكن، أو البدء بـ MTA من البداية إن لم تكن تحتاج STA.
q["كيف تتجنّب تعليق STA"] --> a1["أدر حلقة رسائل على خيط STA"]
q --> a2["ولِّد واستخدم على خيط واجهة المستخدم"]
q --> a3["إن لم تكن تحتاج STA فابدأ بـ MTA"]
a2 -.-> why["خيط واجهة المستخدم يملك الحلقة من البداية"]
الشكل 17: وسائل التجنّب تُرتَّب في ثلاثة اتجاهات: إدارة الحلقة، أو الانتقال إلى موضع يملك حلقة، أو ترك STA.
6.5. ماذا يعني في النهاية «تشغيل حلقة الرسائل»؟
هذا الذي يفعله خيط واجهة المستخدم في Win32.
while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
في STA تصل الاستدعاءات من خيوط أخرى «محوَّلة». هذه الحلقة (مضخّة الرسائل) هي ما يستلم ذلك التحويل ويمرّره إلى التنفيذ.
flowchart TB
accTitle: دور مضخّة الرسائل
accDescr: مخطّط يبيّن أنّ مضخّة الرسائل هي هذا التكرار: استلام استدعاء حُوِّل من خيط آخر بـ GetMessage، وتمريره إلى التنفيذ بـ DispatchMessage.
fwd["استدعاء حُوِّل"] --> gm["الاستلام بـ GetMessage"]
gm --> dm["التمرير إلى التنفيذ بـ DispatchMessage"]
dm --> gm
الشكل 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».
flowchart TB
accTitle: ثلاث وسائل لإدارة حلقة على STA في الخلفية
accDescr: مخطّط يبيّن أنّ لإدارة حلقة رسائل على STA في الخلفية ثلاث وسائل: استخدام Application.Run الذي يحتاج مرجع WinForms، أو كتابة حلقة GetMessage وDispatchMessage بنفسك، أو استخدام MsgWaitForMultipleObjects الذي ينتظر الرسائل وكائن المزامنة معاً.
want["إدارة حلقة على STA في الخلفية"] --> ar["Application.Run"]
want --> gml["كتابة حلقة GetMessage بنفسك"]
want --> mw["MsgWaitForMultipleObjects"]
ar -.-> dep["يلزم مرجع WinForms ووسيلة إنهاء"]
mw -.-> both["ينتظر الرسائل وكائن المزامنة معاً"]
الشكل 19: طرق إدارة الحلقة ثلاث، وتُختار بحسب طريقة الإنهاء وثقل الاعتماد.
6.7. مثال تعليق آخر: ردّ نداء أثناء استدعاء متزامن
STA ليس «تحويل الاستدعاء» فقط؛ بحسب الوضع يأتي ردّ نداء في الاتجاه المعاكس (خادم ← عميل). ومن بين ذلك نمط وقوع ردّ نداء أثناء استدعاء متزامن نمط تعطّل كلاسيكي.
sequenceDiagram
participant UI as خيط واجهة المستخدم (STA)
participant Server as خادم COM
UI->>Server: DoWork() (استدعاء متزامن)
Note over UI: ينتظر عودة DoWork<br/>(لا يعالج الرسائل)
Server->>UI: ProgressCallback() (ردّ نداء)
Note over UI: أثناء الانتظار<br/>لا يستلم ردّ النداء
Note over Server: ينتظر اكتمال ردّ النداء
Note over UI,Server: كلّ طرف ينتظر الآخر → تعطّل
الشكل 20: خيط واجهة مستخدم ينتظر عودة استدعاء متزامن، وخادم ينتظر اكتمال ردّ نداء، ينتظران بعضهما.
لماذا يسهل أن يصير تعطّلاً:
- خيط واجهة المستخدم يستدعي
DoWork()استدعاءً متزامناً (حظر) - خيط واجهة المستخدم ينتظر العودة (لا يعالج الرسائل)
- الخادم يرسل
ProgressCallback()إلى خيط واجهة المستخدم - خيط واجهة المستخدم أثناء الانتظار لا يستلم ردّ النداء
- الخادم ينتظر اكتمال ردّ النداء
- كلّ طرف ينتظر الآخر → لا يتقدّم أبداً
طول زمن المعالجة غير ذي صلة. نمط «وصول ردّ نداء أثناء استدعاء متزامن» نفسه يسهل أن يصير مشكلة.
تذييل: في 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) | إن صعب، صمّم لتقليل عدد الاستدعاءات نفسه (التمرير مجمّعاً) |
أولوية الحسم عند الحيرة أسهل بهذا الترتيب: «متطلّب الطرف الآخر > وجود واجهة مستخدم > التوازي». الشقّة تُحسم عند أوّل تهيئة ولا يمكن تغييرها لاحقاً، لذلك احسم هذا فقط قبل بدء التنفيذ.
flowchart LR
accTitle: الترتيب الذي تنظر إليه عند الحيرة
accDescr: مخطّط يبيّن أنّه عند الحيرة في تقسيم استخدام STA وMTA يسهل الحسم بالتحقّق بهذا الترتيب: متطلّب مكوّن COM المستخدم، ثمّ وجود واجهة مستخدم، ثمّ التوازي.
p1["متطلّب الطرف الآخر"] -->|"ثمّ"| p2["وجود واجهة مستخدم"]
p2 -->|"ثمّ"| p3["التوازي"]
الشكل 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
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
WinRT هو COM ── IInspectable و.winmd وإسقاط اللغات، ولماذا لا يزال WinUI يركب عقداً ثنائياً
WinRT ليس وقت تشغيل مُداراً؛ بل ABI أُضيفت إليه بيانات وصفية (.winmd) وإسقاطات لغات فوق COM. من علاقة IUnknown وIInspectable إلى تهيئة HW...
ما كائن OLE؟ ── آلية التضمين والربط ومطبّات مستندات الأعمال
حقيقة ميزة تضمين جدول Excel في Word هي كائن OLE. نشرح الفرق بين التضمين والربط، والملفّ المركَّب والتخزين المنظَّم، وآلية In-Place Activa...
كيف تعمل الحافظة والسحب والإفلات ── معالجة نقل بيانات OLE بشكل صحيح في تطبيقات الأعمال
لماذا ينهار لصق Excel ويفشل اللصق بعد إغلاق المصدر: تنسيقات الحافظة، والعرض المؤجَّل، وسحب وإفلات OLE، وسياسات السجلّ ومزامنة السحابة.
هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟ ── واقع محاكاة x64 (Prism) ومكتبات DLL وCOM الأصليّة
نجيب المطوّرين ومسؤولي الأنظمة عن سؤال «هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟». نستعرض آليّة محاكاة x64 (Prism)، والطبقات التي ...
ما ينبغي تنظيمه قبل طلب التعهيد أو التطوير التعاقديّ لتطبيق Windows
قبل تكليف جهة خارجية بتطوير تطبيق Windows تعاقديّاً، إليك النقاط التي ينبغي تنظيمها: تعديل البرمجيّات القائمة، وتكامل الأجهزة، وCOM/Activ...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
الاستشارات التقنية ومراجعة التصميم
ترتيب STA / MTA وحلقة الرسائل وعمليات marshalling يتّصل مباشرة بتقسيم المسؤوليات ومراجعة حدود الخيوط قبل التنفيذ.
دعم إعادة استخدام الأصول القديمة وترحيلها
لأنها أساسيات يصعب تجاوزها عند التعامل مع الأصول القائمة التي تتضمّن COM، فهي تتناسب أيضاً مع استشارة دعم إعادة استخدام الأصول القديمة وترحيلها.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- أيّهما تختار، 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 قبل بدء الخيط. الشقّة تُحسم عند أوّل تهيئة ولا يمكن تغييرها لاحقاً.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.