STA и MTA в COM: модель потоков и как не получить зависание
· Обновлено: · Го Комура · COM, Разработка Windows, STA, MTA, Потоки
История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619629)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). STA и MTA в COM: модель потоков и как не получить зависание. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/01/31/000-sta-mta-com-relationship/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21619629
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21619630
STA/MTA в COM — базовая тема, которую трудно обойти при разработке под Windows и при обращении к COM из .NET. В поиске чаще всего спрашивают: почему UI-поток — STA, что происходит при пересечении апартамента и почему возникает зависание.
Оглавление
- 1. Сначала вывод (одной фразой)
- 2. Шаблоны вызова модели апартаментов (схемы)
- 3. STA (Single-Threaded Apartment)
- 4. MTA (Multi-Threaded Apartment)
- 5. Где фиксируются STA/MTA
- 6. Конкретный пример зависания из-за ошибки со STA
- 7. Грубое разделение по задачам
- 8. Итог
- 9. Источники
Когда пользуются COM, от вопроса «на каком потоке это выполняется» не уйти. В центре стоит модель апартаментов (STA/MTA). STA/MTA — не общее понятие потока Windows, а модель потоков, которая задаёт правила вызова COM-объектов.
В статье связь STA, MTA и COM разбирается схемами и доводится до ответа на вопрос «почему бывает зависание».
flowchart TB
accTitle: Место модели апартаментов
accDescr: STA/MTA — не общее понятие потока Windows, а две формы модели апартаментов, которая задаёт правила вызова COM-объектов.
com["правила вызова COM-объектов"] --> am["модель апартаментов"]
am --> sta["STA"]
am --> mta["MTA"]
am -.-> note["не общее понятие потока Windows"]
Рис. 1: STA/MTA — две формы модели апартаментов, которая задаёт правила вызова COM-объектов.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 20, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
1. Сначала вывод (одной фразой)
- Правила вызова COM-объекта задаёт апартамент, которому он принадлежит
- STA удобно понимать как один апартамент на поток, MTA — как один апартамент на несколько потоков
- Вызов через границу апартамента COM маршалирует через Proxy/Stub
flowchart TB
accTitle: Связь STA, MTA и пересечения апартамента
accDescr: STA — один апартамент на поток, MTA — один апартамент на несколько потоков; вызов через границу апартамента COM маршалирует через Proxy/Stub.
sta["STA (один апартамент на поток)"] -->|"вызов через границу апартамента"| m["маршалинг через Proxy / Stub"]
mta["MTA (один апартамент на несколько потоков)"] -->|"вызов через границу апартамента"| m
Рис. 2: Правила вызова задаёт апартамент принадлежности; маршалинг вставляется только при пересечении апартамента.
2. Шаблоны вызова модели апартаментов (схемы)
У вызова COM-объекта в целом три шаблона.
flowchart TB
accTitle: Три шаблона вызова
accDescr: Вызов COM-объекта бывает трёх видов: внутри того же STA-потока, внутри того же MTA и через границу апартамента.
caller["вызов COM-объекта"] --> p1["шаблон 1: внутри того же STA-потока"]
caller --> p2["шаблон 2: внутри того же MTA"]
caller --> p3["шаблон 3: через границу апартамента"]
Рис. 3: Шаблоны вызова делятся на три, и от того, какой это случай, зависят накладные расходы и то, на что смотреть.
2.1. Шаблон 1: вызов внутри того же STA-потока
Внутри того же STA-потока вызов прямой. Накладных расходов нет.
flowchart LR
subgraph STA[STA-поток]
Caller[вызывающий код]
Obj[COM-объект]
Caller -->|прямой вызов| Obj
end
Рис. 4: Вызов внутри того же STA-потока — прямой, без накладных расходов.
2.2. Шаблон 2: вызов внутри того же MTA
Из нескольких потоков внутри MTA вызвать напрямую можно с любого потока. Но объект обязан быть потокобезопасным.
flowchart LR
subgraph MTA["MTA (один апартамент)"]
Thread1[рабочий поток 1]
Thread2[рабочий поток 2]
Obj[COM-объект]
Thread1 -->|прямой вызов| Obj
Thread2 -->|прямой вызов| Obj
end
Рис. 5: Внутри того же MTA вызов прямой с любого потока, но объект должен быть потокобезопасным.
2.3. Шаблон 3: вызов через границу апартамента
Между разными апартаментами COM передаёт вызов через Proxy/Stub. Для стандартных интерфейсов это делает среда выполнения COM.
В таблице ниже без пояснения появятся слова, специфичные для COM. Сначала по строке на каждое.
| Термин | Смысл |
|---|---|
| Маршалинг | Когда вызов пересекает границу апартамента или процесса, вызов и аргументы упаковывают в форму, которую можно передать как есть, и переносят на ту сторону. За границей их возвращают к исходному виду |
| Proxy / Stub | Пара частей, которая делает маршалинг. Со стороны вызова стоит Proxy (принимает вызов, притворяясь настоящим объектом), со стороны вызываемого — Stub (передаёт принятый вызов настоящему объекту) |
IDispatch |
COM-интерфейс, который спрашивает имя метода строкой и вызывает его по номеру. Скриптовые языки и VBA могут пользоваться COM именно благодаря этому |
| Automation | Общее имя способа пользоваться COM, который опирается на IDispatch и ограниченный набор типов данных (BSTR, VARIANT и т. п.). Если вызов укладывается в этот диапазон, маршалинг берёт на себя oleaut32.dll со стороны ОС |
| Библиотека типов | Данные с машиночитаемым описанием формы интерфейса (методы, типы аргументов). Лежит отдельным файлом .tlb или встроена в DLL / EXE |
| Маршалер библиотеки типов | Стандартная возможность COM: прочитать библиотеку типов и на месте выполнить маршалинг. Отдельный Proxy/Stub не нужен именно благодаря ей |
| MIDL | Компилятор Microsoft, который из определения интерфейса (.idl) генерирует код Proxy/Stub и прочее |
Замечание: Proxy/Stub не готовятся автоматически для чего угодно, но на практике явно генерировать их почти никогда не нужно.
| Шаблон | Подготовка Proxy/Stub |
|---|---|
На базе IDispatch (Automation) |
Не нужна. Обрабатывает oleaut32.dll |
| Библиотека типов уже зарегистрирована | Не нужна. Обрабатывает маршалер библиотеки типов |
| .NET COM Interop | Обычно не нужна. Идёт через библиотеку типов |
Собственный интерфейс, прямо производный от IUnknown |
Нужны генерация и регистрация Proxy/Stub через MIDL |
То есть генерировать Proxy/Stub через MIDL нужно, когда делают интерфейс, прямо производный от IUnknown, без IDispatch.
Для обычных COM-компонентов, которыми пользуются из .NET или скриптовых языков, эта работа нужна редко.
flowchart TB
accTitle: Развилка: готовить ли Proxy/Stub самим
accDescr: Если хватает стандартного маршалинга COM вроде IDispatch или маршалера библиотеки типов, генерировать Proxy/Stub не нужно. Генерация и регистрация через MIDL нужны только для собственного интерфейса, прямо производного от IUnknown, который этим не покрывается.
q{"Хватает стандартного маршалинга?"} -->|"да"| auto["генерировать Proxy/Stub не нужно"]
q -->|"нет"| midl["сгенерировать и зарегистрировать Proxy/Stub через MIDL"]
auto -.-> how["обрабатывают IDispatch или библиотека типов"]
auto -.-> few["на практике почти всегда этот случай"]
Рис. 6: Явно генерировать Proxy/Stub нужно только для собственного интерфейса, прямо производного от IUnknown, который стандартный маршалинг не покрывает.
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: Вызов через границу апартамента передаётся через Proxy, RPC и Stub, которые среда выполнения COM готовит сама.
Важно: При пересечении апартамента появляются накладные расходы маршалинга. На частых вызовах это бьёт по производительности, поэтому учитывать это нужно ещё на этапе проектирования.
2.4. Порядок накладных расходов маршалинга
Ниже — обычный порядок величины (это не измеренные значения; сильно зависят от ситуации и сложности параметров).
| Шаблон вызова | Порядок времени | Относительное ощущение |
|---|---|---|
| Внутри того же апартамента (напрямую) | 10–100 наносекунд | Почти как обычный вызов функции |
| Разные апартаменты (тот же процесс) | 1–10 микросекунд | В 100–1000 раз дольше прямого вызова |
| Разные процессы (Out-of-proc) | 100–1000 микросекунд | В 10 000–100 000 раз дольше прямого вызова |
Относительное сравнение:
- Тот же апартамент: порядка одного обращения к памяти
- Разные апартаменты: порядка одного системного вызова
- Разные процессы: порядка сетевого обмена с localhost
В сценарии вроде 10 000 вызовов в цикле эта разница становится очень заметной.
Как обращаться с этими числами
Таблица выше — чтобы почувствовать порядок величины, а не измерение с источником. Единого опубликованного бенчмарка нет, поэтому сами эти числа в основание проектного решения не кладите.
Зато структура «внутри того же апартамента — прямой вызов, за границей всегда вставляется маршалинг» — спецификация из документации Microsoft. В разделе Single-Threaded Apartments прямо сказано: внутри того же апартамента указатель на интерфейс можно передать без маршалинга; при пересечении апартамента даже внутри одного процесса используется тот же механизм маршалинга, что и между процессами; и вызов приходит как оконное сообщение скрытому окну (OleMainThreadWndClass). Порядок величины меняется из-за этой структуры; сами числа зависят от среды и сложности аргументов.
Если нужно решить на своём случае, надёжнее измерить один и тот же интерфейс из того же апартамента и из другого.
flowchart TB
accTitle: Почему меняется порядок величины
accDescr: Внутри того же апартамента вызов прямой, а при пересечении апартамента даже в том же процессе всегда вставляется маршалинг. Если адресат STA, вызов приходит как оконное сообщение скрытому окну. Это структурная причина смены порядка величины.
same["вызов внутри того же апартамента"] --> direct["прямой вызов"]
cross["вызов через границу апартамента"] --> marshal["всегда вставляется маршалинг"]
marshal -.-> msg["для STA приходит скрытому окну"]
Рис. 8: Порядок величины меняется из-за спецификации: за границей всегда вставляется маршалинг.
// C#. Один и тот же вызов повторяют N раз и получают время на один вызов
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");
}
Если измерить и метод с малым числом аргументов, и метод, который принимает строку или массив, видно и то, что «эффект зависит от объёма маршалинга».
3. STA (Single-Threaded Apartment)
STA — модель «один поток = один апартамент».
- COM-объекты в этом апартаменте по сути выполняются только на этом потоке
- Вызов с другого потока COM передаёт через очередь сообщений / RPC
- Часто используют на UI-потоке (WinForms/WPF): у UI тоже «привязка к одному потоку плюс цикл сообщений», поэтому сочетание естественное
flowchart TB
accTitle: Модель выполнения STA
accDescr: В STA COM-объект апартамента по сути выполняется только на потоке, который его создал; вызов с другого потока COM передаёт этому потоку через очередь сообщений или RPC.
obj["COM-объект STA"] --> own["выполняется только на потоке, который создал"]
other["вызов с другого потока"] --> fwd["передача через очередь сообщений / RPC"]
fwd --> own
Рис. 9: Объект STA выполняется только на своём потоке; вызов с другого потока доходит уже переданным.
3.1. Почему на UI-потоке используют STA
Потому что UI-поток и STA устроены одинаково.
- Элементы UI не потокобезопасны Кнопки, текстовые поля и т. п. безопасно менять только с потока, который их создал
- STA — та же «привязка к одному потоку» COM-объект выполняется напрямую только на потоке, который его создал
- UI-поток всегда крутит цикл сообщений Это нужно, чтобы обрабатывать оконные события. Совпадает с предпосылкой STA (насос сообщений)
Поэтому UI-поток WinForms/WPF по умолчанию STA.
flowchart TB
accTitle: Совпадение устройства UI-потока и STA
accDescr: Элементы UI безопасно трогать только с потока, который их создал. У STA та же привязка к одному потоку. Цикл сообщений, который UI-поток крутит всегда, совпадает с предпосылкой STA — насосом сообщений. Поэтому UI-поток по умолчанию STA.
ui["привязка элементов UI к одному потоку"] --> match["устройство совпадает"]
sta["привязка STA к одному потоку"] --> match
loop["цикл сообщений UI-потока"] --> pre["предпосылка STA (насос сообщений)"]
pre --> match
match --> def["UI-поток по умолчанию STA"]
Рис. 10: UI-поток стал STA, потому что совпадают два устройства: привязка к одному потоку и цикл сообщений.
Важно: У STA сильная привязка к потоку, зато при большом числе вызывающих сторон легко образуется очередь.
4. MTA (Multi-Threaded Apartment)
MTA — модель «несколько потоков — один апартамент».
- COM-объект вызывают одновременно с нескольких потоков
- Объект обязан быть потокобезопасным
- Подходит для серверной и фоновой обработки
Важно: У MTA высокий параллелизм, но ответственность реализации объекта тяжёлая.
flowchart TB
accTitle: Модель выполнения MTA
accDescr: MTA делит один апартамент между несколькими потоками, COM-объект вызывают одновременно с нескольких потоков, поэтому объект обязан быть потокобезопасным.
mta["MTA (несколько потоков — один апартамент)"] --> par["вызывают одновременно с нескольких потоков"]
par --> safe["объект обязан быть потокобезопасным"]
mta -.-> use["подходит для серверной и фоновой обработки"]
Рис. 11: MTA можно вызывать параллельно, зато ответственность за безопасность переходит на реализацию объекта.
5. Где фиксируются STA/MTA
Апартамент COM фиксируется инициализацией каждого потока.
- В момент вызова
CoInitialize/CoInitializeExапартамент этого потока фиксируется - STA:
COINIT_APARTMENTTHREADED - MTA:
COINIT_MULTITHREADED
flowchart TB
accTitle: Момент, когда фиксируется апартамент
accDescr: Апартамент потока фиксируется в момент вызова CoInitialize или CoInitializeEx: COINIT_APARTMENTTHREADED даёт STA, COINIT_MULTITHREADED — MTA.
th["поток"] --> init["вызов CoInitialize / CoInitializeEx"]
init -->|"COINIT_APARTMENTTHREADED"| sta["фиксируется STA"]
init -->|"COINIT_MULTITHREADED"| mta["фиксируется MTA"]
Рис. 12: Апартамент фиксируется указанием в момент инициализации каждого потока.
5.1. STA/MTA в .NET
В .NET есть атрибуты [STAThread] / [MTAThread] и ApartmentState, но это обёртки, которые задают модель апартаментов 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-объект
- Этот поток не крутит цикл сообщений
- 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-потоку. Передача приходит как оконное сообщение скрытому окну (класс окна
OleMainThreadWndClass), которое COM создаёт для этого апартамента - Чтобы принять эту передачу, 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() — это та же форма.
UI-поток, напротив, с самого начала крутит цикл сообщений, чтобы обрабатывать оконные события, и выполняет требование STA без дополнительной реализации. Поэтому UI-поток — естественное место для 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-поток всё равно нужно освободить. Без этого
// останется поток переднего плана (foreground), который ждёт 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-поток по умолчанию является потоком переднего плана (foreground), поэтому пока 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-поток принимает вызовы с другого потока, он должен крутить цикл сообщений
- По возможности создавать и использовать объект на UI-потоке (у UI-потока цикл сообщений есть с самого начала)
- Если STA не нужен — с самого начала брать MTA
Дополнение: если всё остаётся внутри одного потока, Application.Run() не всегда обязателен.
Но в коде вокруг UI и COM вызовы с другого потока встречаются так часто, что на практике это почти обязательно.
flowchart TB
accTitle: Три направления, как избежать зависания
accDescr: Если принимают вызов с другого потока — крутить цикл сообщений на STA-потоке; если можно — создавать и использовать на UI-потоке, у которого цикл уже есть; если STA не нужен — с самого начала брать MTA.
q["как избежать зависания STA"] --> a1["крутить цикл сообщений на STA-потоке"]
q --> a2["создавать и использовать на UI-потоке"]
q --> a3["если STA не нужен — сразу MTA"]
a2 -.-> why["у UI-потока цикл есть с самого начала"]
Рис. 17: Обходы сводятся к трём направлениям: крутить цикл, перенести туда, где цикл уже есть, отказаться от STA.
6.5. Что на деле значит «крутить цикл сообщений»?
То самое, что делает каждый UI-поток 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 UI-поток (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: UI-поток ждёт возврата синхронного вызова, сервер ждёт завершения обратного вызова — они ждут друг друга.
Почему это так легко даёт взаимную блокировку:
- UI-поток делает синхронный (блокирующий) вызов
DoWork() - UI-поток ждёт возврата (сообщения не обрабатывает)
- Сервер отправляет
ProgressCallback()на UI-поток - UI-поток стоит в ожидании, поэтому обратный вызов принять нельзя
- Сервер ждёт завершения обратного вызова
- Каждый ждёт другого → дальше никто не движется
Длительность обработки ни при чём. Легко ломается сам шаблон «обратный вызов во время синхронного вызова».
Дополнение: у COM в некоторых ситуациях есть механизмы крутить сообщения и допускать повторный вход, поведение зависит от компонента и формы вызова. Взаимная блокировка бывает не всегда, но этот шаблон лучше не строить.
7. Грубое разделение по задачам
| Ситуация | Что брать | Почему так | Что ещё нужно |
|---|---|---|---|
| Есть UI (WinForms / WPF) | STA | У элементов UI привязка к одному потоку, у UI-потока цикл сообщений уже есть (3.1) | Ничего особенного. По умолчанию это STA |
| Нужна массовая параллельная обработка | MTA | Несколько потоков делят один апартамент и вызывают напрямую, без передачи (2.2) | Потокобезопасная реализация объекта COM. Это ответственность реализации объекта, а не взаимного исключения на стороне вызова |
| Нужен COM STA в фоне | STA + цикл сообщений | Раз вызывают с другого потока, нужен вход, который примет передачу (6.2) | Application.Run() или цикл GetMessage. Способ остановить (ExitThread и т. п.) проектируют вместе (6.6) |
| Требование используемого COM-компонента уже известно | Подстроиться под другую сторону | Апартамент и есть правило вызова, потом его не сменить (глава 5) | Смотрят значение ThreadingModel. Если Apartment — собирают в расчёте на STA |
| Вызывают часто | Сдвинуть в тот же апартамент, что и вызывающая сторона | На каждом пересечении границы вставляется маршалинг (2.4) | Если это трудно — проектируют так, чтобы самих вызовов стало меньше (передавать пакетом) |
Если сомневаетесь, порядок приоритета «требование другой стороны > есть ли UI > параллелизм» обычно быстрее даёт ответ. Апартамент фиксируется первой инициализацией и потом не меняется, поэтому это решают до начала реализации.
flowchart LR
accTitle: В каком порядке смотреть при сомнении
accDescr: Если сомневаетесь в выборе STA или MTA, обычно быстрее решить, глядя по порядку: требование COM-компонента, есть ли UI, параллелизм.
p1["требование другой стороны"] -->|"затем"| p2["есть ли UI"]
p2 -->|"затем"| p3["параллелизм"]
Рис. 21: Если сомневаетесь в разделении, проверяйте требование другой стороны, наличие UI и параллелизм — в этом порядке.
8. Итог
STA/MTA — модель потоков для COM: STA — один поток = один апартамент, MTA — несколько потоков на один апартамент. Вызов через границу апартамента COM передаёт через Proxy/Stub (для нестандартных интерфейсов нужны генерация и регистрация через MIDL и аналоги), но там появляются накладные расходы маршалинга, поэтому там, где ждут частые вызовы, модель апартаментов стоит продумать заранее.
С точки зрения зависания всё сводится к одному: «STA-поток, который принимает вызовы с другого потока, должен крутить насос сообщений». Вызов STA-потока, который сообщения не крутит, легко зависает, а шаблон «обратный вызов во время синхронного вызова» легко даёт взаимную блокировку. У UI-потока с самого начала есть и «привязка к одному потоку», и «цикл сообщений», поэтому эти предпосылки он выполняет без дополнительной реализации и хорошо сочетается с 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 (почему цикл сообщений обязателен, скрытое окно, предупреждение о взаимной блокировке в .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
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Ловушки регистрации и bitness при разработке COM/OCX/ActiveX
Практический разбор типичных ловушек COM, OCX и ActiveX: разрядность 32/64-бит, Visual Studio 2022, regsvr32 и Regasm, права администрато...
Что такое Reg-Free COM: механизм использования COM без регистрации
Разбираем основы Reg-Free COM: роль контекста активации и манифестов, преимущества, ограничения и критерии выбора в реальных проектах.
Как построить вывод отчётов Excel: COM / Open XML / шаблоны
Проектирование вывода отчётов Excel сильно меняется в зависимости от того, автоматизируете ли вы сам Excel, собираете ли xlsx напрямую ил...
Введение в Media Foundation: как понять API через COM
Разбираем, что такое Media Foundation, вместе с базовой терминологией Windows-медиа-API — COM, HRESULT, IMFSourceReader, MFT — в том поря...
Пример COM-моста: вызов 64-битной DLL из 32-битного приложения
32-битное приложение не может напрямую вызвать 64-битную DLL. Разбираем, как связать их COM-мостом: ограничение Windows, схема и ход вызова.
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Миграция ActiveX
Решения о сохранении, обёртке или замене компонентов COM / ActiveX / OCX.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Технические консультации и ревью дизайна
Разбор STA/MTA, цикла сообщений и маршалинга напрямую связан с разделением ответственности до начала реализации и с ревью границ потоков.
Использование и перенос существующих активов
Это основа, которую трудно обойти, когда в существующих наработках есть COM, поэтому тема хорошо сочетается и с консультацией по повторному использованию кода и поддержке миграции.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что выбрать: STA или MTA?
- Если в работе есть UI — STA, если нужна массовая параллельная обработка — MTA. Это базовое разделение. STA — один апартамент на поток, привязка к потоку сильная, но при большом числе вызывающих сторон легко образуется очередь. MTA делит один апартамент между несколькими потоками, параллелизм выше, но объект COM обязан быть потокобезопасным. Если ситуация не из этих двух, реалистично следовать требованию существующей библиотеки или COM-сервера.
- Почему UI-поток — это STA?
- Потому что UI-поток и STA устроены одинаково. Элементы UI вроде кнопок и текстовых полей не потокобезопасны: безопасно обращаться к ним можно только с потока, который их создал. STA — та же модель привязки к одному потоку. Кроме того, UI-поток всегда крутит цикл сообщений, чтобы обрабатывать оконные события, и тем самым без дополнительной реализации выполняет предпосылку STA — насос сообщений. Поэтому UI-поток WinForms/WPF по умолчанию STA.
- Почему вызов COM-объекта в STA зависает?
- Вызов COM-объекта STA обрабатывается на STA-потоке, который его создал. Вызов с другого потока COM передаёт через сообщение или RPC, но если STA-поток не крутит цикл сообщений, передачу принять нельзя, вызывающая сторона ждёт и зависает. Чтобы этого не было, крутят цикл сообщений на STA-потоке, который вызывают с другого потока, создают и используют объект на UI-потоке либо, если STA не нужен, с самого начала берут MTA.
- Зачем в .NET атрибут [STAThread]?
- Это обёртка, которая задаёт модель апартаментов COM. Если поставить его на Main, поток инициализируется как STA, когда используется COM. До реального вызова COM инициализации нет, поэтому в приложении без COM эффекта нет. На дополнительно созданные потоки атрибут не действует — для них до старта вызывают Thread.SetApartmentState. Апартамент фиксируется первой инициализацией и потом не меняется.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.