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, что происходит при пересечении апартамента и почему возникает зависание.

Оглавление


Когда пользуются COM, от вопроса «на каком потоке это выполняется» не уйти. В центре стоит модель апартаментов (STA/MTA). STA/MTA — не общее понятие потока Windows, а модель потоков, которая задаёт правила вызова COM-объектов.

В статье связь STA, MTA и COM разбирается схемами и доводится до ответа на вопрос «почему бывает зависание».

Место модели апартаментовSTA/MTA — не общее понятие потока Windows, а две формы модели апартаментов, которая задаёт правила вызова COM-объектов.правила вызова COM-объектовмодель апартаментовSTAMTAне общее понятие потока Windows

Рис. 1: STA/MTA — две формы модели апартаментов, которая задаёт правила вызова COM-объектов.

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 20, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

1. Сначала вывод (одной фразой)

  • Правила вызова COM-объекта задаёт апартамент, которому он принадлежит
  • STA удобно понимать как один апартамент на поток, MTA — как один апартамент на несколько потоков
  • Вызов через границу апартамента COM маршалирует через Proxy/Stub
Связь STA, MTA и пересечения апартаментаSTA — один апартамент на поток, MTA — один апартамент на несколько потоков; вызов через границу апартамента COM маршалирует через Proxy/Stub.вызов через границу апартаментавызов через границу апартаментаSTA (один апартамент на поток)маршалинг через Proxy / StubMTA (один апартамент на несколько потоков)

Рис. 2: Правила вызова задаёт апартамент принадлежности; маршалинг вставляется только при пересечении апартамента.

2. Шаблоны вызова модели апартаментов (схемы)

У вызова COM-объекта в целом три шаблона.

Три шаблона вызоваВызов COM-объекта бывает трёх видов: внутри того же STA-потока, внутри того же MTA и через границу апартамента.вызов COM-объекташаблон 1: внутри того же STA-потокашаблон 2: внутри того же MTAшаблон 3: через границу апартамента

Рис. 3: Шаблоны вызова делятся на три, и от того, какой это случай, зависят накладные расходы и то, на что смотреть.

2.1. Шаблон 1: вызов внутри того же STA-потока

Внутри того же STA-потока вызов прямой. Накладных расходов нет.

STA-потокпрямой вызоввызывающий кодCOM-объект

Рис. 4: Вызов внутри того же STA-потока — прямой, без накладных расходов.

2.2. Шаблон 2: вызов внутри того же MTA

Из нескольких потоков внутри MTA вызвать напрямую можно с любого потока. Но объект обязан быть потокобезопасным.

MTA (один апартамент)прямой вызовпрямой вызоврабочий поток 1COM-объектрабочий поток 2

Рис. 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 или скриптовых языков, эта работа нужна редко.

Развилка: готовить ли Proxy/Stub самимЕсли хватает стандартного маршалинга COM вроде IDispatch или маршалера библиотеки типов, генерировать Proxy/Stub не нужно. Генерация и регистрация через MIDL нужны только для собственного интерфейса, прямо производного от IUnknown, который этим не покрывается.данетХватает стандартного маршалинга?генерировать Proxy/Stub не нужносгенерировать и зарегистрировать Proxy/Stub через MIDLобрабатывают IDispatch или библиотека типовна практике почти всегда этот случай

Рис. 6: Явно генерировать Proxy/Stub нужно только для собственного интерфейса, прямо производного от IUnknown, который стандартный маршалинг не покрывает.

MTA-потоксреда выполнения COM (автоматически)STA-потоквызовпередачаCOM-объектProxyRPC/IPCStubвызывающий код

Рис. 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). Порядок величины меняется из-за этой структуры; сами числа зависят от среды и сложности аргументов.

Если нужно решить на своём случае, надёжнее измерить один и тот же интерфейс из того же апартамента и из другого.

Почему меняется порядок величиныВнутри того же апартамента вызов прямой, а при пересечении апартамента даже в том же процессе всегда вставляется маршалинг. Если адресат STA, вызов приходит как оконное сообщение скрытому окну. Это структурная причина смены порядка величины.вызов внутри того же апартаментапрямой вызоввызов через границу апартаментавсегда вставляется маршалингдля 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 тоже «привязка к одному потоку плюс цикл сообщений», поэтому сочетание естественное
Модель выполнения STAВ STA COM-объект апартамента по сути выполняется только на потоке, который его создал; вызов с другого потока COM передаёт этому потоку через очередь сообщений или RPC.COM-объект STAвыполняется только на потоке, который создалвызов с другого потокапередача через очередь сообщений / RPC

Рис. 9: Объект STA выполняется только на своём потоке; вызов с другого потока доходит уже переданным.

3.1. Почему на UI-потоке используют STA

Потому что UI-поток и STA устроены одинаково.

  • Элементы UI не потокобезопасны Кнопки, текстовые поля и т. п. безопасно менять только с потока, который их создал
  • STA — та же «привязка к одному потоку» COM-объект выполняется напрямую только на потоке, который его создал
  • UI-поток всегда крутит цикл сообщений Это нужно, чтобы обрабатывать оконные события. Совпадает с предпосылкой STA (насос сообщений)

Поэтому UI-поток WinForms/WPF по умолчанию STA.

Совпадение устройства UI-потока и STAЭлементы UI безопасно трогать только с потока, который их создал. У STA та же привязка к одному потоку. Цикл сообщений, который UI-поток крутит всегда, совпадает с предпосылкой STA — насосом сообщений. Поэтому UI-поток по умолчанию STA.привязка элементов UI к одному потокуустройство совпадаетпривязка STA к одному потокуцикл сообщений UI-потокапредпосылка STA (насос сообщений)UI-поток по умолчанию STA

Рис. 10: UI-поток стал STA, потому что совпадают два устройства: привязка к одному потоку и цикл сообщений.

Важно: У STA сильная привязка к потоку, зато при большом числе вызывающих сторон легко образуется очередь.

4. MTA (Multi-Threaded Apartment)

MTA — модель «несколько потоков — один апартамент».

  • COM-объект вызывают одновременно с нескольких потоков
  • Объект обязан быть потокобезопасным
  • Подходит для серверной и фоновой обработки

Важно: У MTA высокий параллелизм, но ответственность реализации объекта тяжёлая.

Модель выполнения MTAMTA делит один апартамент между несколькими потоками, COM-объект вызывают одновременно с нескольких потоков, поэтому объект обязан быть потокобезопасным.MTA (несколько потоков — один апартамент)вызывают одновременно с нескольких потоковобъект обязан быть потокобезопаснымподходит для серверной и фоновой обработки

Рис. 11: MTA можно вызывать параллельно, зато ответственность за безопасность переходит на реализацию объекта.

5. Где фиксируются STA/MTA

Апартамент COM фиксируется инициализацией каждого потока.

  • В момент вызова CoInitialize / CoInitializeEx апартамент этого потока фиксируется
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED
Момент, когда фиксируется апартаментАпартамент потока фиксируется в момент вызова CoInitialize или CoInitializeEx: COINIT_APARTMENTTHREADED даёт STA, COINIT_MULTITHREADED — MTA.COINIT_APARTMENTTHREADEDCOINIT_MULTITHREADEDпотоквызов CoInitialize / CoInitializeExфиксируется STAфиксируется MTA

Рис. 12: Апартамент фиксируется указанием в момент инициализации каждого потока.

5.1. STA/MTA в .NET

В .NET есть атрибуты [STAThread] / [MTAThread] и ApartmentState, но это обёртки, которые задают модель апартаментов COM.

  • [STAThread]ставят на метод Main (точку входа). При использовании COM поток инициализируется как STA
  • [MTAThread] → тоже для Main. Инициализируется как MTA
  • Thread.SetApartmentState(ApartmentState.STA)для дополнительно создаваемых потоков. Задать нужно до старта потока

Оговорки:

  • Даже при [STAThread] до реального вызова COM инициализации нет (в приложении без COM эффекта нет)
  • На дополнительные потоки [STAThread] не действует. Используют Thread.SetApartmentState

То есть STA/MTA в .NET — это те же STA/MTA COM, механизм, подготовленный для COM Interop.

Важно: Потом апартамент сменить нельзя. Первая инициализация решает всё.

Где в .NET задают апартаментНа Main вешают атрибут STAThread или MTAThread, на дополнительно создаваемый поток до старта вызывают Thread.SetApartmentState; в обоих случаях апартамент фиксируется первой инициализацией и потом не меняется.метод Mainатрибут 〔STAThread〕 / 〔MTAThread〕дополнительно создаваемый потокдо старта: Thread.SetApartmentStateпервая инициализация фиксирует апартаментпотом сменить нельзя

Рис. 13: В точке входа — атрибут, на дополнительном потоке — SetApartmentState; в обоих случаях фиксируется с первого раза.

6. Конкретный пример зависания из-за ошибки со STA

Конфигурация ниже на практике легко даёт зависание.

6.1. Типичная ситуация

  • В фоне создают STA-поток и на нём создают COM-объект
  • Этот поток не крутит цикл сообщений
  • COM-объект вызывают с другого потока (неважно, STA это или MTA)
Конфигурация, которая легко даёт зависаниеФоновый STA-поток создал COM-объект, но не крутит цикл сообщений, и этот объект вызывают с другого потока — конфигурация, которая легко даёт зависание.вызываетфоновый STA-потоксоздаёт COM-объектне крутит цикл сообщенийдругой поток (STA или MTA)

Рис. 14: Опасное сочетание: «STA без цикла держит объект, который вызывают с другого потока».

6.2. Что происходит

Причину зависания можно свести к двум предпосылкам STA. Этот раздел объясняет причину; дальше её не повторяем.

  • COM-объект обрабатывается на STA-потоке, который его создал Вызывающая сторона может быть STA или MTA — вызов с другого потока всегда передаётся этому STA-потоку. Передача приходит как оконное сообщение скрытому окну (класс окна OleMainThreadWndClass), которое COM создаёт для этого апартамента
  • Чтобы принять эту передачу, STA-поток должен крутить насос сообщений В документации Microsoft прямо сказано: «каждый STA должен иметь цикл сообщений, чтобы обрабатывать вызовы из других процессов и из других апартаментов того же процесса»

Следовательно, STA-поток, который не крутит сообщения, вызов принять не может, вызывающая сторона ждёт ответа и в результате зависает.

Ход, который приводит к зависаниюВызов с другого потока передаётся создавшему объект STA-потоку как оконное сообщение скрытому окну, но если STA-поток не крутит насос сообщений, принять его нельзя, вызывающая сторона ждёт ответа и зависает.вызов с другого потокапередача STA-потоку, который создал объектприходит как сообщение скрытому окнунасос сообщений не крутитсясторона STA вызов не принимаетвызывающая сторона ждёт ответазависание

Рис. 15: Передача приходит сообщением, поэтому у STA с остановленным насосом некому её принять.

Это же верно и для .NET. В документации Single-Threaded Apartments есть предупреждение: если заблокировать STA-поток через Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne() и т. п., обратный вызов COM и вызов через границу апартамента не смогут завершиться, и получится взаимная блокировка. Пример сбоя в следующем пункте 6.3 как раз останавливается на WaitOne() — это та же форма.

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.

Среда выполнения COMSTA-потокГлавный потокСреда выполнения COMSTA-потокГлавный потокЦикла сообщений нетЗастряло здесьПередаёт сообщением, но...Стоит на WaitOne,сообщения не обрабатываетВызывающая сторона тоже продолжает ждатьОба ждут → зависаниеСтарт потокаCoInitializeEx (STA)Создание COM-объектаready.Set()Ожидание на done.WaitOne()CallComObject()Пытается передать вызов

Рис. 16: STA-поток, остановившийся на WaitOne, и главный поток, ждущий ответа на передачу, не могут продвинуться друг без друга.

Две строки в центре схемы, «передаёт сообщением, но…», — место, где ломаются две предпосылки из 6.2.

6.4. Как этого избежать

  • Если STA-поток принимает вызовы с другого потока, он должен крутить цикл сообщений
  • По возможности создавать и использовать объект на UI-потоке (у UI-потока цикл сообщений есть с самого начала)
  • Если STA не нужен — с самого начала брать MTA

Дополнение: если всё остаётся внутри одного потока, Application.Run() не всегда обязателен. Но в коде вокруг UI и COM вызовы с другого потока встречаются так часто, что на практике это почти обязательно.

Три направления, как избежать зависанияЕсли принимают вызов с другого потока — крутить цикл сообщений на STA-потоке; если можно — создавать и использовать на UI-потоке, у которого цикл уже есть; если STA не нужен — с самого начала брать MTA.как избежать зависания STAкрутить цикл сообщений на STA-потокесоздавать и использовать на UI-потокеесли STA не нужен — сразу MTAу UI-потока цикл есть с самого начала

Рис. 17: Обходы сводятся к трём направлениям: крутить цикл, перенести туда, где цикл уже есть, отказаться от STA.

6.5. Что на деле значит «крутить цикл сообщений»?

То самое, что делает каждый UI-поток Win32.

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

В STA вызов с другого потока приходит «передачей». Этот цикл (насос сообщений) принимает передачу и отдаёт её на выполнение.

Роль насоса сообщенийПереданный с другого потока вызов принимают через GetMessage и отдают на выполнение через DispatchMessage — этот повтор и есть насос сообщений.переданный вызовпринять через GetMessageотдать на выполнение через DispatchMessage

Рис. 18: «Крутить цикл сообщений» — это этот повтор: принять передачу и отдать на выполнение.

6.6. Пример верного направления (если набросать)

Если нужен «COM на фоновом STA», форма такая.

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

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

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

    // Пока STA-поток жив, крутим сообщения
    Application.Run();

    CoUninitialize();
});

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

ready.WaitOne();
CallComObject(comObj);

(※ Забыть вызвать CoInitializeEx / CoUninitialize — обычный способ сломать себе жизнь. Объявление P/Invoke для CoInitializeEx нужно то же, что в 6.3.)

Форма Application.Run() без аргументов как раз крутит цикл сообщений без формы. Для этой задачи она и задумана, но два момента лучше держать в голове.

  • Нужна ссылка на System.Windows.Forms. В консольном приложении в файл проекта добавляют <UseWindowsForms>true</UseWindowsForms>
  • Сам этот цикл не заканчивается. Чтобы остановить, на том же потоке вызывают Application.ExitThread() (или Application.Exit(), если завершают всё приложение). Если в примере выше нужно дойти до CoUninitialize(), отдельно нужен механизм, который передаст STA-потоку «завершись» и заставит вызвать ExitThread

Если зависеть от WinForms не хочется, пишут цикл GetMessage / DispatchMessage из 6.5 сами или берут MsgWaitForMultipleObjects, который умеет ждать и сообщения, и объект синхронизации. Второй вариант — обычный выбор, когда нужно «принять уведомление о завершении по событию и при этом не терять вызовы COM».

Три способа крутить цикл на фоновом STAЦикл сообщений на фоновом STA крутят через Application.Run (нужна ссылка на WinForms), через свой цикл GetMessage и DispatchMessage или через MsgWaitForMultipleObjects, который ждёт и сообщения, и объект синхронизации.крутить цикл на фоновом STAApplication.Runнаписать цикл GetMessage самимMsgWaitForMultipleObjectsнужны ссылка на WinForms и способ остановитьждёт и сообщения, и объект синхронизации

Рис. 19: Крутить цикл можно тремя способами; выбирают по тому, как останавливать и насколько тяжела зависимость.

6.7. Ещё один пример зависания: обратный вызов во время синхронного вызова

STA — это не только «вызов передаётся»: в зависимости от ситуации обратный вызов идёт и в обратную сторону (сервер → клиент). Среди них шаблон, когда обратный вызов приходит во время синхронного вызова, — классика взаимной блокировки.

COM-серверUI-поток (STA)COM-серверUI-поток (STA)Ждёт возврата из DoWork(сообщения не обрабатывает)Стоит в ожидании,обратный вызов принять нельзяЖдёт завершения обратного вызоваКаждый ждёт другого → взаимная блокировкаDoWork() (синхронный вызов)ProgressCallback() (обратный вызов)

Рис. 20: UI-поток ждёт возврата синхронного вызова, сервер ждёт завершения обратного вызова — они ждут друг друга.

Почему это так легко даёт взаимную блокировку:

  1. UI-поток делает синхронный (блокирующий) вызов DoWork()
  2. UI-поток ждёт возврата (сообщения не обрабатывает)
  3. Сервер отправляет ProgressCallback() на UI-поток
  4. UI-поток стоит в ожидании, поэтому обратный вызов принять нельзя
  5. Сервер ждёт завершения обратного вызова
  6. Каждый ждёт другого → дальше никто не движется

Длительность обработки ни при чём. Легко ломается сам шаблон «обратный вызов во время синхронного вызова».

Дополнение: у 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 > параллелизм» обычно быстрее даёт ответ. Апартамент фиксируется первой инициализацией и потом не меняется, поэтому это решают до начала реализации.

В каком порядке смотреть при сомненииЕсли сомневаетесь в выборе STA или MTA, обычно быстрее решить, глядя по порядку: требование COM-компонента, есть ли UI, параллелизм.затемзатемтребование другой стороныесть ли UIпараллелизм

Рис. 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

Скачать Word-версию этой статьи

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что выбрать: 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог