STA ו-MTA ב-COM —‏ threading model, ואיך נמנעים מ-hang

· עודכן בתאריך: · · COM, פיתוח Windows, STA, MTA, threads

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 31 Jan 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173261)

מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.

Go Komura (2026). STA ו-MTA ב-COM —‏ threading model, ואיך נמנעים מ-hang. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173261 https://comcomponent.com/he/blog/sta-mta-com-relationship/

DOI (הגרסה האחרונה)
10.5281/zenodo.22173261
DOI (הגרסה הזו)
10.5281/zenodo.22173262

STA/MTA ב-COM הם ידע בסיסי שקשה להימנע ממנו כשעובדים עם Windows או ניגשים ל-COM מתוך .NET. השאלות הנפוצות ביותר בחיפושים הן: למה UI thread הוא STA, מה קורה כשחוצים apartment, ולמה נוצר hang.

תוכן עניינים


כשמשתמשים ב-COM, אי אפשר להתעלם מהשאלה “באיזה thread זה רץ”. במרכז הנושא נמצא Apartment Model (STA/MTA). STA/MTA אינם מושג כללי של threads ב-Windows, אלא threading model שקובע את כללי הקריאה לאובייקטי COM.

במאמר הזה נסביר את הקשר בין STA, MTA ו-COM באמצעות תרשימים, ונגיע גם להסבר “למה קורה hang”.

מיקומו של Apartment ModelSTA/MTA אינם מושג כללי של threads ב-Windows, אלא שתי הצורות של Apartment Model שקובע את כללי הקריאה לאובייקטי COM.כללי הקריאה לאובייקטי COMApartment ModelSTAMTAלא מושג כללי של threads ב-Windows

איור 1: STA/MTA הן שתי הצורות של Apartment Model, שקובע את כללי הקריאה לאובייקטי COM.

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 20, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

1. קודם המסקנה (במשפט אחד)

  • לאובייקט COM נקבעים כללי הקריאה לפי “לאיזה apartment הוא שייך”
  • קל יותר להבין את STA כ-apartment אחד לכל thread, ואת MTA כ-apartment אחד המשותף לכמה threads
  • קריאה שחוצה apartment COM מבצע לה marshaling דרך Proxy/Stub
הקשר בין STA, MTA וחציית apartmentSTA הוא apartment אחד לכל thread, ו-MTA הוא apartment אחד המשותף לכמה threads, וקריאה שחוצה apartment עוברת marshaling דרך Proxy/Stub על ידי COM.קריאה שחוצה apartmentקריאה שחוצה apartmentSTA (apartment אחד לכל thread)marshaling דרך Proxy / StubMTA (apartment אחד לכמה threads)

איור 2: ה-apartment שאליו האובייקט שייך קובע את כללי הקריאה, ו-marshaling נכנס לתמונה רק כשחוצים apartment.

2. תבניות הקריאה של Apartment Model (תרשים)

לקריאה לאובייקט COM יש בגדול שלוש תבניות.

שלוש תבניות הקריאהלקריאה לאובייקט COM יש שלוש תבניות — קריאה בתוך אותו STA thread, קריאה בתוך אותו MTA, וקריאה שחוצה apartment.קריאה לאובייקט COMתבנית 1: בתוך אותו STA threadתבנית 2: בתוך אותו MTAתבנית 3: חוצה apartment

איור 3: יש שלוש תבניות קריאה, וההשתייכות לאחת מהן קובעת את התקורה ואת נקודות תשומת הלב.

2.1. תבנית 1: קריאה בתוך אותו STA thread

בתוך אותו STA thread אפשר לקרוא ישירות. אין תקורה.

STA threadקריאה ישירהקוד קוראאובייקט COM

איור 4: קריאה בתוך אותו STA thread היא קריאה ישירה, בלי תקורה.

2.2. תבנית 2: קריאה בתוך אותו MTA

מכמה threads בתוך MTA אפשר לקרוא ישירות מכל thread. אבל צד האובייקט חייב תכנון thread-safe.

MTA (apartment אחד)קריאה ישירהקריאה ישירהworker thread 1אובייקט COMworker thread 2

איור 5: בתוך אותו MTA אפשר לקרוא ישירות מכל thread, אבל האובייקט חייב תכנון thread-safe.

2.3. תבנית 3: קריאה שחוצה apartment

בין apartments שונים, COM מעביר את הקריאה באמצעות Proxy/Stub. עבור interfaces סטנדרטיים, ה-COM runtime מטפל בזה בשבילכם.

מכאן ואילך מופיעות בטבלה מילים ייחודיות ל-COM בלי הסבר. נסדר אותן קודם, שורה אחר שורה.

מונח משמעות
Marshaling packing מחדש של הקריאה והארגומנטים לצורה שאפשר “להעביר כמות שהיא” כדי לחצות גבול של apartment או process. בצד השני של הגבול מוחזרים לצורתם המקורית
Proxy / Stub זוג רכיבים שאחראים על ה-marshaling. Proxy עומד בצד ה-caller (מעמיד פנים שהוא האובייקט האמיתי ומקבל את הקריאה), Stub עומד בצד הנקרא (מעביר את הקריאה שהתקבלה לאובייקט האמיתי)
IDispatch COM interface שמאפשר לשאול שם method כמחרוזת ולקרוא לה לפי מספר. בזכות המנגנון הזה אפשר להשתמש ב-COM משפות סקריפט או מ-VBA
Automation שם כולל לשימוש ב-COM שמניח IDispatch וטיפוסי נתונים מוגבלים שאפשר להשתמש בהם שם (BSTR, VARIANT וכדומה). כשנשארים בטווח הזה, את ה-marshaling מבצע oleaut32.dll שבצד ה-OS
Type library נתונים שכותבים בהם את צורת ה-interface (methods, טיפוסי ארגומנטים) בצורה קריאה למכונה. מונחים כקובץ .tlb נפרד, או מוטמעים בתוך DLL / EXE
Type library marshaler פונקציונליות סטנדרטית ב-COM שקוראת type library ומבצעת marshaling במקום. בזכות זה לא צריך ליצור Proxy/Stub ייעודי
MIDL compiler של Microsoft שיוצר, בין השאר, קוד Proxy/Stub מתוך הגדרת interface (.idl)

שימו לב: לא הכול נוצר אוטומטית עבור Proxy/Stub, אבל בפועל ברוב המקרים אין צורך ליצור אותם במפורש.

תבנית הכנת Proxy/Stub
מבוסס IDispatch (Automation) לא נדרש. oleaut32.dll מטפל בזה
Type library רשומה לא נדרש. ה-type library marshaler מטפל בזה
COM Interop ב-.NET בדרך כלל לא נדרש. פועל דרך type library
interface מותאם שיורש ישירות מ-IUnknown נדרשת יצירה ורישום של Proxy/Stub עם MIDL

כלומר, יצירת Proxy/Stub עם MIDL נדרשת רק כשבונים interface שיורש ישירות מ-IUnknown בלי להשתמש ב-IDispatch. ברכיבי COM רגילים שמשתמשים בהם מ-.NET או משפות סקריפט, נדרשת העבודה הזו לעיתים רחוקות.

נקודת ההכרעה אם להכין Proxy/Stub בעצמכםאם marshaling סטנדרטי כמו IDispatch או type library marshaler מספיק, אין צורך ביצירת Proxy/Stub, ורק כשאלה לא מספיקים — עבור interface מותאם שיורש ישירות מ-IUnknown — נדרשת יצירה ורישום עם MIDL.כןלאmarshaling סטנדרטי מספיק?אין צורך ביצירת Proxy/Stubיצירה ורישום עם MIDLIDispatch או type library מטפלים בזהברוב המקרים בפועל זה המצב

איור 6: יוצרים Proxy/Stub במפורש רק כש-marshaling סטנדרטי לא מספיק — עבור interface מותאם שיורש ישירות מ-IUnknown.

MTA threadCOM runtime (אוטומטי)STA threadקריאההעברהאובייקט COMProxyRPC/IPCStubקוד קורא

איור 7: קריאה שחוצה apartment עוברת דרך Proxy, RPC ו-Stub שה-COM runtime מכין אוטומטית, ומגיעה ליעד.

נקודה מרכזית: חציית apartment יוצרת תקורת marshaling. בקריאות בתדירות גבוהה יש לזה השפעה על הביצועים, ולכן צריך להביא זאת בחשבון בזמן התכנון.

2.4. אומדן התקורה של marshaling

להלן אומדן כללי (לא ערכים שנמדדו בפועל, ומשתנה מאוד לפי המצב ומורכבות הפרמטרים).

תבנית קריאה זמן משוער תחושה יחסית
בתוך אותו apartment (ישיר) 10-100 ננו-שנייה כמעט זהה לקריאת פונקציה רגילה
apartment שונה (אותו process) 1-10 מיקרו-שנייה פי 100-1000 מקריאה ישירה
process שונה (Out-of-proc) 100-1000 מיקרו-שנייה פי 10,000-100,000 מקריאה ישירה

השוואה יחסית:

  • אותו apartment: בסדר גודל של גישה אחת לזיכרון
  • apartment שונה: בסדר גודל של system call אחד
  • process שונה: בסדר גודל של תקשורת רשת ל-localhost

בתרחיש שבו קוראים 10,000 פעמים בלולאה, ההבדל הזה מורגש מאוד.

לגבי הטיפול במספרים האלה

הטבלה למעלה נועדה לתת תחושה של סדר הגודל, ואינה ערכים נמדדים עם מקור מאומת. אין גם benchmark אחיד שפורסם, ולכן אין להשתמש במספרים האלה כשלעצמם כבסיס להחלטת תכנון.

מצד שני, המבנה שבו “בתוך אותו apartment יש קריאה ישירה, וחציית גבול תמיד כרוכה ב-marshaling” הוא מפרט שכתוב בתיעוד של Microsoft. בפרק על Single-Threaded Apartments כתוב במפורש שבתוך אותו apartment אפשר להעביר interface pointer בלי marshaling, שכאשר חוצים apartment נעשה שימוש באותו מנגנון marshaling כמו בין processes גם אם זה בתוך אותו process, ושהקריאה מגיעה כ-window message ל-hidden window (OleMainThreadWndClass). סדר הגודל משתנה בגלל המבנה הזה, והמספרים עצמם משתנים לפי הסביבה ומורכבות הארגומנטים.

אם רוצים להחליט לפי המקרה שלכם, הדרך הבטוחה היא למדוד ולהשוות בין קריאה לאותו interface מתוך אותו apartment לבין קריאה מ-apartment אחר.

הסיבה המבנית לשינוי בסדר הגודלקריאה בתוך אותו apartment היא קריאה ישירה, בעוד קריאה שחוצה apartment תמיד כרוכה ב-marshaling גם בתוך אותו process, וכשהיעד הוא STA הקריאה מגיעה כ-window message ל-hidden window.קריאה בתוך אותו apartmentקריאה ישירהקריאה שחוצה apartmentתמיד כרוכה ב-marshalingליעד STA מגיעה כהודעה ל-hidden window

איור 8: השינוי בסדר הגודל נובע מהמבנה שבו חציית גבול תמיד כרוכה ב-marshaling.

// C#. חוזרים על אותה קריאה N פעמים ומחשבים את הזמן לקריאה אחת
static void Measure(string label, Action call, int iterations = 100_000)
{
    call(); // מוציאים מהמדידה את העיכוב הראשוני (JIT, יצירת proxy, כינון חיבור)

    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");
}

אם מודדים גם method עם מעט ארגומנטים וגם method שמעבירה מחרוזת או מערך, אפשר לראות ש”ההשפעה משתנה לפי כמות ה-marshaling”.

3. STA (Single-Threaded Apartment)

STA הוא מודל של “thread אחד = apartment אחד”.

  • אובייקטי COM בתוך אותו apartment רצים בעיקרון רק באותו thread
  • קריאה מ-thread אחר COM מעביר אותה דרך message queue/RPC
  • נפוץ ב-UI thread (WinForms/WPF) (גם ל-UI יש “thread affinity + message loop”, ולכן ההתאמה טובה)
מודל ההרצה של STAאובייקט COM ב-STA רץ בעיקרון רק ב-thread שיצר אותו, וקריאה מ-thread אחר מועברת על ידי COM דרך message queue או RPC לאותו thread.אובייקט COM ב-STAרץ רק ב-thread שיצר אותוקריאה מ-thread אחרמועברת דרך message queue / RPC

איור 9: אובייקט STA רץ רק ב-thread הבעלים שלו, וקריאה מבחוץ מגיעה לאחר העברה.

3.1. למה UI thread משתמש ב-STA

כי התכנון של UI thread ושל STA תואם.

  • UI controls אינם thread-safe כפתורים ותיבות טקסט וכדומה אפשר לגעת בהם בבטחה רק מה-thread שיצר אותם
  • גם STA הוא “thread affinity” אובייקט COM רץ ישירות רק ב-thread שיצר אותו
  • UI thread תמיד מריץ message loop זה הכרחי לטיפול ב-window events. זה תואם לתנאי המוקדם של STA (message pump)

לכן UI thread ב-WinForms/WPF הוא STA כברירת מחדל.

התאמת התכנון בין UI thread ל-STAUI controls אפשר לגעת בהם בבטחה רק מה-thread שיצר אותם, ל-STA יש אותה thread affinity, ו-message loop ש-UI thread תמיד מריץ תואם לתנאי המוקדם של STA — message pump — ולכן UI thread הוא STA כברירת מחדל.thread affinity של UI controlsהתאמת תכנוןthread affinity של STAmessage loop של UI threadהתנאי המוקדם של STA (message pump)UI thread הוא STA כברירת מחדל

איור 10: כי התכנון תואם בשתי נקודות — thread affinity ו-message loop —‏ UI thread הוא STA.

נקודה מרכזית: ל-STA יש thread affinity חזקה, אבל בתמורה, כשיש הרבה callers קל שנוצר בו עומס.

4. MTA (Multi-Threaded Apartment)

MTA הוא מודל של “apartment אחד לכמה threads”.

  • אובייקט COM נקרא בו-זמנית מכמה threads
  • חובה תכנון thread-safe בצד האובייקט
  • מתאים לעיבוד בצד שרת או עיבוד ברקע

נקודה מרכזית: ל-MTA יש מקביליות גבוהה, אבל האחריות על מימוש האובייקט כבדה יותר.

מודל ההרצה של MTAMTA משתף apartment אחד בין כמה threads, ואובייקט COM נקרא בו-זמנית מכמה threads, מה שמחייב תכנון thread-safe בצד האובייקט.MTA (apartment אחד לכמה threads)נקרא בו-זמנית מכמה threadsחובה תכנון thread-safe בצד האובייקטמתאים לעיבוד בצד שרת או ברקע

איור 11: ל-MTA יש קריאה מקבילית, אבל האחריות על הבטיחות עוברת למימוש האובייקט.

5. איפה נקבע STA/MTA

Apartment של COM נקבע על ידי אתחול לכל thread.

  • ברגע שקוראים ל-CoInitialize / CoInitializeEx, נקבע ה-apartment של אותו thread
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED
הרגע שבו נקבע ה-apartmentברגע שה-thread קורא ל-CoInitialize או ל-CoInitializeEx נקבע ה-apartment שלו, ו-COINIT_APARTMENTTHREADED קובע STA בעוד COINIT_MULTITHREADED קובע MTA.COINIT_APARTMENTTHREADEDCOINIT_MULTITHREADEDthreadקורא ל-CoInitialize / CoInitializeExנקבע STAנקבע MTA

איור 12: ה-apartment נקבע לפי הציון שנעשה ברגע קריאת האתחול, לכל thread בנפרד.

5.1. STA/MTA ב-.NET

גם ל-.NET יש את ה-attributes [STAThread] / [MTAThread] ואת ApartmentState, אבל אלה wrappers להגדרת ה-apartment model של COM.

  • [STAThread] → מוסיפים ל-Main (נקודת הכניסה). מאותחל כ-STA כשמשתמשים ב-COM
  • [MTAThread] → גם הוא עבור Main. מאותחל כ-MTA
  • Thread.SetApartmentState(ApartmentState.STA) → עבור threads נוספים שיוצרים. יש להגדיר לפני התחלת ה-thread

נקודות לתשומת לב:

  • גם אם יש [STAThread], האתחול לא מתבצע עד לקריאה בפועל ל-COM (אין השפעה באפליקציה שלא משתמשת ב-COM)
  • [STAThread] לא פועל על threads נוספים. יש להשתמש ב-Thread.SetApartmentState

כלומר, ה-STA/MTA של .NET הם STA/MTA של COM עצמם, מנגנון שהוכן עבור COM Interop.

חשוב: אי אפשר לשנות את ה-apartment לאחר מכן. האתחול הראשון קובע הכול.

היכן קובעים apartment ב-.NETל-Main מוסיפים attribute STAThread או MTAThread, ול-threads נוספים שיוצרים מגדירים לפני ההתחלה עם Thread.SetApartmentState, ובשני המקרים ה-apartment נקבע באתחול הראשון ולא ניתן לשנות אותו לאחר מכן.שיטת Mainattribute [STAThread] / [MTAThread]thread נוסף שנוצרלפני ההתחלה: Thread.SetApartmentStateהאתחול הראשון קובע את ה-apartmentלא ניתן לשנות לאחר מכן

איור 13: בנקודת הכניסה זה נעשה ב-attribute, ול-thread נוסף עם SetApartmentState, ובשניהם ההגדרה נקבעת בפעם הראשונה בלבד.

6. דוגמה קונקרטית ל-hang שקורה כשטועים ב-STA

מבנה כמו הבא גורם בפועל בקלות ל-hang.

6.1. מצב נפוץ

  • יוצרים STA thread ברקע ומייצרים בו אובייקט COM
  • ה-thread הזה אינו מריץ message loop
  • קוראים לאובייקט ה-COM הזה מ-thread אחר (בלי קשר אם הוא STA או MTA)
מבנה שגורם בקלות ל-hangSTA thread שנוצר ברקע ייצר אובייקט COM אבל אינו מריץ message loop, ו-thread אחר קורא לאובייקט COM הזה — מבנה שגורם בקלות ל-hang.קוראSTA thread ברקעמייצר אובייקט COMאינו מריץ message loopthread אחר (STA או MTA)

איור 14: השילוב של “STA שלא מריץ loop, שמחזיק אובייקט שקוראים לו מ-thread אחר” מסוכן.

6.2. מה קורה בפועל

את סיבת ה-hang אפשר לצמצם לשני תנאים מוקדמים של STA. הפרק הזה מסביר את הסיבה, ובפרקים הבאים לא נחזור עליה.

  • אובייקט COM מטופל ב-STA thread שיצר אותו בין אם ה-caller הוא STA או MTA, קריאה מ-thread אחר תמיד מועברת לאותו STA thread. ההעברה מגיעה כ-window message ל-hidden window (window class OleMainThreadWndClass) ש-COM יוצר עבור אותו apartment
  • כדי לקבל את ההעברה, STA thread חייב להריץ message pump גם בתיעוד של Microsoft כתוב במפורש ש”כל STA חייב שתהיה לו message loop, כדי לטפל בקריאות מ-processes אחרים או מ-apartment אחר באותו process”

לכן STA thread שלא מריץ messages לא יכול לקבל את הקריאה, ה-caller ממשיך להמתין לתשובה, וכתוצאה מכך נוצר hang.

הזרימה שמובילה ל-hangקריאה מ-thread אחר מועברת ל-STA thread שיצר את האובייקט כהודעה ל-hidden window, אבל אם STA thread לא מריץ message pump הוא לא מקבל אותה, וה-caller ממשיך להמתין לתשובה ונכנס ל-hang.קריאה מ-thread אחרמועברת ל-STA thread שיצר את האובייקטמגיעה כהודעה ל-hidden windowmessage pump לא רצהצד ה-STA לא מקבל את הקריאהה-caller ממשיך להמתין לתשובהhang

איור 15: ההעברה מגיעה כהודעה, ולכן כשה-pump עצורה ב-STA אין מי שיקבל אותה.

זה נכון גם ב-.NET. בתיעוד של Single-Threaded Apartments יש אזהרה שאם חוסמים STA thread עם Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne() וכדומה, callback של COM או קריאה שחוצה apartment לא יכולים להסתיים, וזה גורם ל-deadlock. דוגמת הכשל בסעיף 6.3 הבא, שנעצרת ב-WaitOne(), היא בדיוק הצורה הזו.

מצד שני, UI thread מריץ message loop מלכתחילה כדי לטפל ב-window events, ולכן הוא ממלא את דרישת STA בלי מימוש נוסף. זו הסיבה ש-UI thread הוא בחירה טבעית להריץ בו אובייקטי 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();

            // ממתין בלי message loop -> כאן נמצא הפגם הקטלני
            done.WaitOne();

            CoUninitialize();
        });

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

        ready.WaitOne();

        try
        {
            // קריאה מ-thread אחר (בין אם STA או MTA) מועברת ל-STA
            // אבל צד ה-STA לא מטפל בהודעות, ולכן קל שכאן נוצר hang
            dynamic obj = comObj;
            obj.AnyMethod();
        }
        finally
        {
            // גם אם הקריאה חוזרת (המחלקה הייתה agile, הקריאה לא הועברה,
            // AnyMethod חזרה מיד וכו') - חשוב תמיד לשחרר את STA thread.
            // בלי זה, נשאר foreground thread שממתין ל-done, וגם כש"לא שוחזר
            // ה-hang" ה-process לא מסתיים. אי אפשר להבחין בין שני המצבים
            // לפי הסימפטום
            done.Set();
            staThread.Join();
        }
    }
}

הקוד הזה מניח שמעבירים ProgID של מחלקת COM שנרשמה כ-ThreadingModel=Apartment (= STA). יש להחליף את AnyMethod בשם method שקיימת בפועל באותה מחלקה. זה לא משוחזר עם כל מחלקת COM. שלושת התנאים הנדרשים לשחזור הם:

תנאי איך בודקים
ה-ThreadingModel של המחלקה הוא Apartment בודקים את הערך ThreadingModel תחת HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32 ב-Registry. אם זה Both או Free, הקריאה לא מועברת, ולא ישוחזר
STA thread לא מריץ messages זה בדיוק מה ש-done.WaitOne() בדוגמה למעלה עושה
הקריאה מתבצעת מ-thread אחר כל עוד קוראים מתוך אותו STA thread, זו קריאה ישירה, ולא נוצר hang

הסיבה ש-obj.AnyMethod() עטוף ב-try / finally, עם done.Set() ו-staThread.Join() תמיד מתבצעים, היא כדי להבחין ב”מקרה שלא שוחזר”. STA thread הוא כברירת מחדל foreground thread, ולכן ה-process לא מסתיים כל עוד done לא הוגדר. אם משמיטים את ה-finally, הסימפטום שנראה מבחוץ יהיה “ה-process לא מסתיים” גם כשה-hang שוחזר (הקריאה לא חוזרת) וגם כשלא (הקריאה חזרה אבל אף אחד לא הגדיר את done). הכלי שנועד לבדוק את תנאי השחזור מסתיר בעצמו את הצלחת התנאי. עם הצורה למעלה, כשלא משוחזר, ה-process מסתיים כרגיל.

כדי לוודא שיש hang, הדרך המהירה היא לעצור את ה-process בדיבאגר ולבדוק שה-stack של thread הקורא עצור בהמתנה של COM, וש-STA thread עצור ב-WaitOne.

COM runtimeSTA threadmain threadCOM runtimeSTA threadmain threadאין message loopתקוע כאןמעביר בהודעה, אבל...בגלל WaitOneלא יכול לטפל בהודעהגם ה-caller ממשיך להמתיןשני הצדדים ממתינים -> hangהתחלת threadCoInitializeEx (STA)יצירת אובייקט COMready.Set()ממתין עם done.WaitOne()CallComObject()מנסה להעביר את הקריאה

איור 16: STA thread שנעצר ב-WaitOne ו-main thread שממתין לתשובת ההעברה — שניהם לא יכולים להתקדם.

במרכז התרשים, שתי השורות “מעביר בהודעה, אבל…” הן המקום שבו שני התנאים המוקדמים שהוזכרו בסעיף 6.2 נשברים.

6.4. עקרונות למניעה

  • כשמקבלים קריאות מ-thread אחר, STA thread חייב להריץ message loop
  • אם אפשר, ליצור ולהשתמש ב-UI thread (ל-UI thread כבר יש message loop מלכתחילה)
  • אם STA לא נחוץ, להשתמש מלכתחילה ב-MTA

הערה: אם הכול נשאר בתוך אותו thread, לא תמיד Application.Run() הכרחי. אבל כי ב-UI וב-COM לרוב מעורבת קריאה מ-thread אחר, זה כמעט הכרחי בפועל.

שלושה כיווני מניעה של hangשלושת כיווני המניעה של hang הם — להריץ message loop ב-STA thread כשמקבלים קריאות מ-thread אחר, ליצור ולהשתמש באובייקט ב-UI thread שיש לו כבר loop, ואם STA לא נחוץ — להשתמש מלכתחילה ב-MTA.איך נמנעים מ-hang של STAלהריץ message loop ב-STA threadליצור ולהשתמש ב-UI threadאם STA לא נחוץ — MTA מלכתחילהל-UI thread כבר יש loop

איור 17: אפשר לסדר את דרכי המניעה בשלושה כיוונים — להריץ loop, לעבור למקום שיש בו loop, או לוותר על STA.

6.5. מה זה בעצם “להריץ message loop”?

זה בדיוק מה ש-UI thread ב-Win32 עושה.

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

ב-STA, קריאה מ-thread אחר “מועברת” לכאן. הלולאה הזו (message pump) היא זו שמקבלת את ההעברה ומעבירה אותה להרצה.

תפקיד ה-message pumpmessage pump היא החזרה על קבלת הקריאה שהועברה מ-thread אחר עם GetMessage והעברתה להרצה עם DispatchMessage.קריאה שהועברה מ-thread אחרמתקבלת עם GetMessageמועברת להרצה עם DispatchMessage

איור 18: “להריץ message loop” הוא בדיוק החזרה הזו של קבלת ההעברה והעברתה להרצה.

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 thread חי, מריצים messages
    Application.Run();

    CoUninitialize();
});

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

ready.WaitOne();
CallComObject(comObj);

(לשכוח לקרוא ל-CoInitializeEx / CoUninitialize זו תקלה רגילה. הצהרת ה-P/Invoke עבור CoInitializeEx נדרשת זהה לזו שבסעיף 6.3)

הצורה של Application.Run() בלי ארגומנטים נועדה להריץ רק message loop בלי טופס. זה בדיוק המקרה שאליו זה מיועד, אבל כדאי לזכור שתי נקודות:

  • נדרשת הפניה ל-System.Windows.Forms. באפליקציית קונסולה, מוסיפים <UseWindowsForms>true</UseWindowsForms> לקובץ הפרויקט
  • הלולאה הזו לא מסתיימת מעצמה. כדי לעצור אותה, קוראים ל-Application.ExitThread() (או ל-Application.Exit() שמסיים את כל האפליקציה) מתוך אותו thread. אם רוצים להגיע ל-CoUninitialize() בדוגמה למעלה, צריך מנגנון נפרד שמעביר “סיום” ל-STA thread ומריץ בו ExitThread

אם לא רוצים תלות ב-WinForms, אפשר לכתוב בעצמכם לולאה של GetMessage / DispatchMessage כמו בסעיף 6.5, או להשתמש ב-MsgWaitForMultipleObjects, שמאפשר להמתין גם להודעות וגם לאובייקטי סנכרון. זו הבחירה הרגילה כשרוצים “לקבל הודעת סיום דרך event, בלי לפספס קריאות COM”.

שלושה אמצעים להרצת loop ב-STA ברקעשלושת האמצעים להרצת message loop ב-STA ברקע הם Application.Run שדורש הפניה ל-WinForms, כתיבה עצמית של לולאת GetMessage ו-DispatchMessage, או שימוש ב-MsgWaitForMultipleObjects שממתין גם להודעות וגם לאובייקטי סנכרון.הרצת loop ב-STA ברקעApplication.Runכתיבה עצמית של לולאת GetMessageMsgWaitForMultipleObjectsדורש הפניה ל-WinForms ואמצעי סיוםממתין גם להודעות וגם לאובייקטי סנכרון

איור 19: יש שלוש דרכים להריץ loop, ובוחרים לפי אופן הסיום ומשקל התלות.

6.7. עוד דוגמת hang: callback במהלך קריאה סינכרונית

ל-STA לא רק “מעבירים אליו קריאות” — במצבים מסוימים מגיע גם callback בכיוון ההפוך (משרת ל-client). בפרט, תבנית שבה callback קורה במהלך קריאה סינכרונית היא דוגמה קלאסית ל-deadlock.

שרת COMUI thread (STA)שרת COMUI thread (STA)ממתין לתשובה של DoWork(לא מטפל בהודעות)בגלל שממתין,לא יכול לקבל את ה-callbackממתין לסיום ה-callbackשני הצדדים ממתינים זה לזה -> deadlockDoWork() (קריאה סינכרונית)ProgressCallback() (callback)

איור 20: UI thread שממתין לתשובה של קריאה סינכרונית, ושרת שממתין לסיום callback — ממתינים זה לזה.

למה קל שנוצר deadlock:

  1. UI thread קורא ל-DoWork() בקריאה סינכרונית (חוסמת)
  2. UI thread ממתין לתשובה (לא מטפל בהודעות)
  3. השרת שולח ProgressCallback() ל-UI thread
  4. כי UI thread ממתין, הוא לא יכול לקבל את ה-callback
  5. השרת ממתין לסיום ה-callback
  6. שני הצדדים ממתינים זה לזה -> לא מתקדם לעולם

אורך זמן העיבוד לא רלוונטי. התבנית עצמה שבה callback מגיע במהלך קריאה סינכרונית נוטה להיות בעייתית.

הערה: ל-COM יש גם מנגנוני הרצת הודעות ו-reentrancy שמשתנים לפי המצב, וההתנהגות משתנה לפי הרכיב וצורת הקריאה. זה לא תמיד גורם ל-deadlock, אבל עדיף להימנע מהתבנית הזו.

7. חלוקה גסה בין השימושים

מצב מה בוחרים סיבת ההחלטה מה נדרש גם
מעורב UI (WinForms / WPF) STA ל-UI controls יש thread affinity, ול-UI thread כבר יש message loop מלכתחילה (3.1) כלום במיוחד. STA כברירת מחדל
רוצים הרבה עיבוד מקבילי MTA כמה threads משתפים apartment אחד וקוראים ישירות בלי העברה (2.2) תכנון thread-safe בצד אובייקט ה-COM. זו אחריות צד המימוש של האובייקט, לא חסימה בצד ה-caller
רוצים להשתמש ב-COM מסוג STA ברקע STA + message loop כל עוד קוראים מ-thread אחר, נדרש פתח לקבלת ההעברה (6.2) Application.Run() או לולאת GetMessage. יש לתכנן גם אמצעי סיום (כמו ExitThread) (6.6)
הדרישה של רכיב ה-COM שבו משתמשים כבר קבועה מתאימים לצד השני ה-apartment הוא כלל הקריאה עצמו, ואי אפשר לשנות אותו לאחר מכן (פרק 5) בודקים את הערך ThreadingModel. אם זה Apartment, בונים בהנחת STA
קריאה בתדירות גבוהה מתקרבים לאותו apartment של ה-caller בכל חציית גבול נכנס marshaling (2.4) אם קשה — לתכנן צמצום של מספר הקריאות עצמו (העברה מרוכזת)

כשמתלבטים, קל יותר להחליט לפי סדר העדיפויות “דרישת הצד השני > קיום UI > מקביליות”. כי ה-apartment נקבע באתחול הראשון ולא ניתן לשנות אותו אחר כך, כדאי להחליט על זה עוד לפני תחילת המימוש.

הסדר שבודקים כשמתלבטיםכשמתלבטים בין STA ל-MTA, קל יותר להחליט אם בודקים לפי הסדר — דרישת רכיב ה-COM שבו משתמשים, קיום UI, ומקביליות.אחר כךאחר כךדרישת הצד השניקיום UIמקביליות

איור 21: כשמתלבטים בין השימושים, בודקים לפי הסדר — דרישת הצד השני, קיום UI, ומקביליות.

8. סיכום

STA/MTA הם threading model עבור COM: STA הוא thread אחד = apartment אחד, ו-MTA הוא apartment אחד המשותף לכמה threads. קריאה שחוצה apartment מועברת על ידי COM דרך Proxy/Stub (עבור interfaces שאינם סטנדרטיים נדרשת יצירה ורישום, למשל עם MIDL), אבל זה כרוך בתקורת marshaling, ולכן במקומות שבהם צפויות קריאות בתדירות גבוהה כדאי להחליט בזהירות על תכנון ה-apartment.

מבחינת hang, הכול מצטמצם לנקודה אחת: “STA thread שמקבל קריאות מ-thread אחר חייב להריץ message pump”. קריאה ל-STA thread שלא מריץ messages נוטה לגרום ל-hang, וגם תבנית שבה מגיע callback במהלך קריאה סינכרונית נוטה לגרום ל-deadlock. ל-UI thread יש מלכתחילה גם “thread affinity” וגם “message loop”, ולכן הוא ממלא את התנאי הזה בלי מימוש נוסף, וזו הסיבה שהוא מתאים כל כך ל-COM מסוג STA.

9. מקורות

  • Apartment Model 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 (הסיבה ש-message loop הכרחית, ה-hidden window, אזהרת deadlock ב-.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 (הצורה של הרצת message loop בלי טופס, ואיך עוצרים אותה) 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?
אם יש UI —‏ STA. אם צריך הרבה עיבוד מקבילי — MTA. זו החלוקה הבסיסית. ל-STA יש thread affinity חזקה (apartment אחד ל-thread), אבל כשיש הרבה callers היא נוטה להיחסם. MTA משתף apartment אחד בין כמה threads, כך שיש מקביליות גבוהה, אבל זה מחייב תכנון thread-safe בצד אובייקט ה-COM. אם המצב אינו אחד מהם, מעשי להתאים לדרישה של הספרייה הקיימת או שרת ה-COM שבו משתמשים.
למה UI thread הוא STA?
כי התכנון של UI thread ושל STA תואם. UI controls כמו כפתורים ותיבות טקסט אינם thread-safe, ואפשר לגעת בהם בבטחה רק מה-thread שיצר אותם. גם STA הוא מודל של thread affinity. בנוסף, UI thread תמיד מריץ message loop כדי לטפל ב-window events, וכך הוא ממלא בלי מימוש נוסף את התנאי המוקדם של STA —‏ message pump. לכן UI thread ב-WinForms/WPF הוא STA כברירת מחדל.
למה קריאה לאובייקט COM ב-STA תוקעת (hang)?
קריאה לאובייקט COM ב-STA מטופלת ב-STA thread שיצר אותו. קריאה מ-thread אחר מועברת על ידי COM דרך message/RPC, אבל אם ה-STA thread לא מריץ message loop הוא לא יכול לקבל את ההעברה, וה-caller ממשיך להמתין ונכנס ל-hang. כדי להימנע מזה, מריצים message loop ב-STA thread שנקרא מ-thread אחר, או יוצרים ומשתמשים בו ב-UI thread, או אם STA לא נחוץ — משתמשים מלכתחילה ב-MTA.
לשם מה קיים ה-attribute [STAThread] ב-.NET?
זו wrapper שמגדירה את ה-apartment model של COM. כשמוסיפים אותה ל-Main, ה-thread מאותחל כ-STA כשמשתמשים ב-COM. עם זאת, האתחול לא מתבצע עד לקריאה בפועל ל-COM, ולכן אין לה השפעה באפליקציה שלא משתמשת ב-COM. היא גם לא פועלת על threads נוספים שנוצרים, ולכן משתמשים ב-Thread.SetApartmentState לפני התחלת ה-thread. חשוב גם ש-apartment נקבע באתחול הראשון ולא ניתן לשנות אותו אחר כך.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג