Conocimientos básicos de STA/MTA en COM - El modelo de subprocesos y cómo evitar los bloqueos

· Actualizado el: · · COM, Desarrollo en Windows, STA, MTA, Subprocesos

STA/MTA en COM son conocimientos básicos difíciles de evitar al desarrollar en Windows o al usar COM desde .NET. Las dudas más habituales en las búsquedas son por qué el subproceso de UI usa STA, qué ocurre al cruzar un Apartment y por qué se produce un bloqueo (hang).

Índice


Al usar COM, resulta inevitable preguntarse «en qué subproceso se ejecuta». En el centro de esta cuestión está el Apartment Model (STA/MTA). STA/MTA no es un concepto general de subprocesos de Windows, sino un modelo de subprocesos que determina las reglas de llamada de los objetos COM.

En este artículo explicamos, con ayuda de diagramas, la relación entre STA, MTA y COM, hasta llegar a por qué se pueden producir bloqueos (hangs).

1. Primero, la conclusión (en pocas palabras)

  • Las reglas de llamada de un objeto COM dependen de a qué Apartment pertenece
  • Resulta más fácil de entender si se piensa que STA es 1 Apartment por subproceso y MTA es 1 Apartment compartido por varios subprocesos
  • Las llamadas que cruzan un Apartment son marshalled por COM a través de Proxy/Stub

2. Patrones de llamada del Apartment Model (diagrama)

Existen, a grandes rasgos, tres patrones de llamada para los objetos COM.

2.1. Patrón 1: llamada dentro del mismo subproceso STA

Dentro del mismo subproceso STA se puede hacer una llamada directa, sin overhead.

Llamada directa dentro del mismo subproceso STADiagrama que muestra que, dentro de un mismo subproceso STA, el código llamador invoca directamente al objeto COM sin pasar por marshallingSubproceso STALlamada directaCódigo llamadorObjeto COM

2.2. Patrón 2: llamada dentro del mismo MTA

Desde varios subprocesos dentro de un MTA se puede llamar directamente desde cualquiera de ellos. Sin embargo, el objeto debe tener un diseño seguro para subprocesos (thread-safe).

Llamada dentro del mismo MTADiagrama que muestra que varios subprocesos de trabajo dentro de un mismo MTA pueden llamar directamente al mismo objeto COMMTA(un único Apartment)Llamada directaLlamada directaSubproceso de trabajo 1Objeto COMSubproceso de trabajo 2

2.3. Patrón 3: llamada que cruza Apartments

Entre distintos Apartments, COM reenvía la llamada usando Proxy/Stub. Si se trata de una interfaz estándar, el runtime de COM se encarga de todo.

En las tablas que siguen aparecen términos propios de COM sin explicación previa. Los definimos aquí, uno por uno.

Término Significado
Marshalling Es el proceso de reempaquetar una llamada y sus argumentos en una forma “transportable” para cruzar los límites de un Apartment o un proceso. Al otro lado del límite se restauran a su forma original
Proxy / Stub Es el par de componentes encargado del marshalling. El Proxy se sitúa del lado del llamador (se hace pasar por el objeto real y recibe la llamada), y el Stub se sitúa del lado del objeto llamado (entrega la llamada recibida al objeto real)
IDispatch Es una interfaz COM que permite consultar el nombre de un método como cadena de texto y luego invocarlo mediante un número. Gracias a este mecanismo, los lenguajes de scripting y VBA pueden usar COM
Automation Es el nombre genérico para el uso de COM basado en IDispatch y en el conjunto limitado de tipos de datos disponibles ahí (BSTR, VARIANT, etc.). Dentro de este ámbito, el marshalling lo asume oleaut32.dll, del propio sistema operativo
Biblioteca de tipos Son datos, en un formato legible por máquina, que describen la forma de una interfaz (métodos, tipos de los argumentos). Se guardan como un archivo .tlb independiente o embebidos en una DLL/EXE
Marshaller de biblioteca de tipos Es una función estándar de COM que lee la biblioteca de tipos y realiza el marshalling sobre la marcha. Gracias a esto no hace falta crear un Proxy/Stub específico
MIDL Es el compilador de Microsoft que genera, entre otras cosas, el código del Proxy/Stub a partir de la definición de la interfaz (.idl)

Nota: no es que el Proxy/Stub se prepare automáticamente para cualquier caso, pero en la práctica casi nunca hace falta generarlo explícitamente.

Patrón Preparación del Proxy/Stub
Basado en IDispatch (Automation) No hace falta. La procesa oleaut32.dll
Biblioteca de tipos registrada No hace falta. La procesa el marshaller de biblioteca de tipos
.NET COM Interop Normalmente no hace falta. Funciona a través de la biblioteca de tipos
Interfaz personalizada derivada directamente de IUnknown Hace falta generar y registrar el Proxy/Stub con MIDL

En resumen, generar el Proxy/Stub con MIDL solo hace falta cuando se crea una interfaz derivada directamente de IUnknown sin usar IDispatch. En los componentes COM habituales que se usan desde .NET o desde lenguajes de scripting, rara vez es necesario realizar este trabajo.

Llamada que cruza Apartments a través de Proxy/StubDiagrama que muestra cómo una llamada desde un subproceso STA pasa por el Proxy, el RPC/IPC y el Stub del runtime de COM antes de llegar al objeto COM en un subproceso MTASubproceso MTARuntime de COM(automático)Subproceso STALlamadaReenvíoObjeto COMProxyRPC/IPCStubCódigo llamador

Punto clave: Cruzar un Apartment genera overhead de marshalling. Cuando las llamadas son frecuentes, esto afecta al rendimiento, así que hay que tenerlo en cuenta en el diseño.

2.4. Orden de magnitud del overhead de marshalling

Lo siguiente es una referencia general (no son valores medidos; varían mucho según la situación y la complejidad de los parámetros).

Patrón de llamada Tiempo estimado Sensación relativa
Dentro del mismo Apartment (directo) 10-100 nanosegundos Prácticamente igual que una llamada a función normal
Apartment distinto (mismo proceso) 1-10 microsegundos 100-1000 veces una llamada directa
Proceso distinto (out-of-proc) 100-1000 microsegundos 10 000-100 000 veces una llamada directa

Comparación relativa:

  • Mismo Apartment: del orden de un acceso a memoria
  • Apartment distinto: del orden de una llamada al sistema (system call)
  • Proceso distinto: del orden de una comunicación de red a localhost

En escenarios donde se llama, por ejemplo, 10 000 veces dentro de un bucle, esta diferencia se nota claramente.

Sobre el uso de estos números

La tabla anterior sirve para hacerse una idea del orden de magnitud, no son valores medidos con una fuente verificable. Tampoco existe un benchmark unificado publicado, así que no use estos números por sí solos como base para decisiones de diseño.

Por otro lado, la estructura de que «dentro del mismo Apartment la llamada es directa, y al cruzar el límite siempre se interpone el marshalling» es una especificación documentada oficialmente por Microsoft. En el apartado de Single-Threaded Apartments se indica explícitamente que, dentro del mismo Apartment, se puede pasar un puntero de interfaz sin marshalling; que al cruzar un Apartment se usa el mismo mecanismo de marshalling que entre procesos, incluso dentro del mismo proceso; y que la llamada llega como un mensaje de ventana a una ventana oculta (OleMainThreadWndClass). El cambio de orden de magnitud se debe a esta estructura, mientras que los valores concretos varían según el entorno y la complejidad de los argumentos.

Si desea evaluar su propio caso, lo más fiable es medir y comparar la misma interfaz llamada desde el mismo Apartment y desde un Apartment distinto.

// C#. Repite la misma llamada N veces para obtener el tiempo por llamada
static void Measure(string label, Action call, int iterations = 100_000)
{
    call(); // Excluye del cálculo el retraso inicial (JIT, creación del proxy, establecimiento de conexión)

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

Si mide tanto con métodos que tienen pocos argumentos como con métodos que pasan cadenas o arreglos, también podrá observar que el impacto varía según la cantidad de datos que hay que serializar en el marshalling.

3. STA (Single-Threaded Apartment)

STA es un modelo de «1 subproceso = 1 Apartment».

  • Los objetos COM dentro de ese Apartment se ejecutan, básicamente, solo en ese subproceso
  • Si se llama desde otro subproceso, COM reenvía la llamada a través de una cola de mensajes/RPC
  • Se usa con frecuencia en el subproceso de UI (WinForms/WPF), ya que la UI también tiene «afinidad con un único subproceso + bucle de mensajes», lo que encaja bien

3.1. Por qué se usa STA en el subproceso de UI

Porque el diseño del subproceso de UI y el de STA coinciden.

  • Los controles de UI no son seguros para subprocesos (thread-safe) Los botones, cuadros de texto, etc. solo se pueden manipular de forma segura desde el subproceso que los creó
  • STA también tiene «afinidad con un único subproceso» Los objetos COM se ejecutan directamente solo en el subproceso que los creó
  • El subproceso de UI siempre ejecuta un bucle de mensajes Es imprescindible para procesar los eventos de ventana, y coincide con el requisito de STA (una bomba de mensajes)

Por eso el subproceso de UI de WinForms/WPF usa STA de forma predeterminada.

Punto clave: STA tiene una alta afinidad con el subproceso, pero a cambio tiende a congestionarse cuando hay muchos llamadores.

4. MTA (Multi-Threaded Apartment)

MTA es un modelo de «1 Apartment compartido por varios subprocesos».

  • Los objetos COM pueden ser llamados simultáneamente desde varios subprocesos
  • El objeto debe tener un diseño seguro para subprocesos (thread-safe)
  • Es adecuado para procesamiento del lado del servidor o en segundo plano

Punto clave: MTA ofrece mayor paralelismo, pero recae más responsabilidad sobre la implementación del objeto.

5. Dónde se determina STA/MTA

El Apartment de COM se determina al inicializarlo por cada subproceso.

  • En el momento en que se llama a CoInitialize / CoInitializeEx, queda determinado el Apartment de ese subproceso
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED

5.1. STA/MTA en .NET

.NET también tiene los atributos [STAThread] / [MTAThread] y ApartmentState, pero estos son envoltorios (wrappers) para configurar el Apartment Model de COM.

  • [STAThread] → se aplica al método Main (el punto de entrada). Se inicializa como STA en el momento en que se usa COM
  • [MTAThread] → también se usa en el método Main. Se inicializa como MTA
  • Thread.SetApartmentState(ApartmentState.STA)para los subprocesos adicionales que se creen. Hay que configurarlo antes de iniciar el subproceso

Puntos a tener en cuenta:

  • Aunque se tenga [STAThread], no se inicializa hasta que realmente se llama a COM (no tiene efecto en una aplicación que no usa COM)
  • [STAThread] no afecta a los subprocesos adicionales. Para ellos se usa Thread.SetApartmentState

En resumen, el STA/MTA de .NET es exactamente el STA/MTA de COM, un mecanismo preparado para COM Interop.

Importante: No es posible cambiar el Apartment después. Todo se decide en la primera inicialización.

6. Ejemplo concreto de bloqueo por un mal uso de STA

Una configuración como la siguiente tiende a producir bloqueos (hangs) reales.

6.1. Situación habitual

  • Se crea un subproceso STA en segundo plano y se genera un objeto COM en él
  • Ese subproceso no está ejecutando un bucle de mensajes
  • Se llama a ese objeto COM desde otro subproceso (sea STA o MTA)

6.2. Qué ocurre

El motivo del bloqueo se puede resumir en dos requisitos de STA. Esta sección explica el motivo, y no lo repetiremos en las secciones siguientes.

  • El objeto COM se procesa en el subproceso STA que lo creó Ya sea que el llamador esté en STA o en MTA, una llamada desde otro subproceso siempre se reenvía a ese subproceso STA. El reenvío llega como un mensaje de ventana dirigido a la ventana oculta que COM crea para ese Apartment (con la clase de ventana OleMainThreadWndClass)
  • Para recibir ese reenvío, el subproceso STA debe estar ejecutando una bomba de mensajes La documentación de Microsoft también indica explícitamente que «cada STA debe tener un bucle de mensajes para procesar las llamadas provenientes de otros procesos o de otros Apartments dentro del mismo proceso»

Por lo tanto, un subproceso STA que no está procesando mensajes no puede recibir la llamada, el llamador se queda esperando la respuesta indefinidamente y, como resultado, se produce un bloqueo.

Esto es igual en .NET. La documentación de Single-Threaded Apartments incluye una advertencia: si se bloquea un subproceso STA con Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne(), etc., los callbacks de COM o las llamadas que cruzan un Apartment no pueden completarse y se produce un deadlock. Que el ejemplo de fallo de la sección 6.3 se detenga en WaitOne() es exactamente esta situación.

Por otro lado, el subproceso de UI ya ejecuta un bucle de mensajes desde el principio para procesar los eventos de ventana, por lo que cumple el requisito de STA sin necesidad de implementación adicional. Por eso el subproceso de UI resulta una elección natural para ejecutar objetos COM en STA.

6.3. Pseudocódigo (patrón de fallo típico)

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(() =>
        {
            // Se inicializa como STA
            CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

            // Se asume una clase COM registrada con ThreadingModel=Apartment
            Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
            comObj = Activator.CreateInstance(type);
            ready.Set();

            // Espera sin bucle de mensajes -> aquí está el problema fatal
            done.WaitOne();

            CoUninitialize();
        });

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

        ready.WaitOne();

        try
        {
            // Al llamar desde otro subproceso (sea STA o MTA), la llamada se reenvía a STA
            // Pero como el lado STA no procesa mensajes, aquí es fácil que se produzca un bloqueo
            dynamic obj = comObj;
            obj.AnyMethod();
        }
        finally
        {
            // Si la llamada retorna -- porque la clase era agile,
            // la llamada no se reenvió, o AnyMethod retornó de inmediato -- de todos
            // modos, hay que liberar siempre el subproceso STA. Si se omite esto, queda
            // un subproceso en primer plano esperando a done, y el proceso tampoco
            // termina cuando el problema "no se reprodujo". Ya no se puede distinguir
            // por los síntomas si el problema se reprodujo o no
            done.Set();
            staThread.Join();
        }
    }
}

Este código presupone que se le pasa el ProgID de una clase COM registrada con ThreadingModel=Apartment (= STA). Reemplace AnyMethod por el nombre de un método que realmente tenga esa clase. No se reproduce con cualquier clase COM. Las condiciones necesarias para reproducirlo son las siguientes tres.

Condición Cómo comprobarla
El ThreadingModel de la clase en cuestión es Apartment Consulte el valor ThreadingModel en el registro, en HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32. Si es Both o Free, la llamada no se reenvía y no se reproduce el problema
El subproceso STA no está procesando mensajes En el ejemplo anterior, esto corresponde a done.WaitOne()
La llamada se realiza desde otro subproceso Mientras se llame desde dentro del mismo subproceso STA, la llamada será directa y no se producirá el bloqueo

El motivo de envolver obj.AnyMethod() en try / finally y garantizar que siempre se ejecuten done.Set() y staThread.Join() es poder distinguir este «caso en que no se reproduce». El subproceso STA es, por defecto, un subproceso en primer plano, así que el proceso no termina a menos que se active done. Si se omite el finally, tanto cuando el problema se reproduce (la llamada no retorna) como cuando no se reproduce (la llamada retorna, pero nadie activa done), el síntoma visible desde fuera es el mismo: «el proceso no termina». La propia herramienta usada para verificar la condición de reproducción termina ocultando si esa condición se cumplió o no. Con la forma mostrada arriba, si el problema no se reproduce, el proceso termina con normalidad.

Para confirmar que el proceso está detenido, lo más rápido es pausar el proceso con el depurador y comprobar que la pila del subproceso llamador está detenida en una espera de COM, y que el subproceso STA está detenido en WaitOne.

Secuencia de bloqueo en el ejemplo de fallo de STADiagrama de secuencia que muestra cómo el subproceso principal llama al objeto COM mientras el subproceso STA está bloqueado en done.WaitOne() sin bucle de mensajes, de forma que ninguno de los dos avanza y se produce un bloqueoRuntime de COMSubproceso STASubproceso principalRuntime de COMSubproceso STASubproceso principalSin bucle de mensajesbloqueado aquíReenvía mediante un mensaje, pero...Como está en WaitOne,no puede procesar el mensajeEl llamador también sigue esperandoAmbos en espera → bloqueoInicio del subprocesoCoInitializeEx (STA)Creación del objeto COMready.Set()Espera en done.WaitOne()CallComObject()Intenta reenviar la llamada

En el centro del diagrama, las dos líneas que dicen «reenvía mediante un mensaje, pero…» son el punto donde fallan los dos requisitos descritos en 6.2.

6.4. Puntos clave para evitarlo

  • Si el subproceso STA va a recibir llamadas de otro subproceso, debe ejecutar un bucle de mensajes
  • Si es posible, cree y use el objeto en el subproceso de UI (que ya tiene un bucle de mensajes desde el principio)
  • Si no se necesita STA, use MTA desde el principio

Nota adicional: si todo se resuelve dentro del mismo subproceso, no siempre hace falta Application.Run(). Sin embargo, como los escenarios de UI y de COM suelen involucrar llamadas desde otro subproceso, en la práctica es casi imprescindible.

6.5. ¿Qué significa realmente «ejecutar el bucle de mensajes»?

Es esto mismo que hace el subproceso de UI de Win32.

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

En STA, las llamadas desde otro subproceso llegan como un «reenvío». Este bucle (la bomba de mensajes) es lo que recibe ese reenvío y lo pone en ejecución.

6.6. Ejemplo del enfoque correcto (a grandes rasgos, así)

Si lo que se quiere es «usar COM en un STA en segundo plano», la forma sería la siguiente.

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

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

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

    // Mientras el subproceso STA esté vivo, procesa mensajes
    Application.Run();

    CoUninitialize();
});

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

ready.WaitOne();
CallComObject(comObj);

(Nota: olvidar llamar a CoInitializeEx / CoUninitialize es una fuente habitual de errores. La declaración P/Invoke de CoInitializeEx es la misma que en 6.3)

La forma de Application.Run() sin argumentos sirve para ejecutar solo el bucle de mensajes, sin tener un formulario. Este es precisamente el escenario para el que está pensada, pero tenga en cuenta estos dos puntos:

  • Se necesita una referencia a System.Windows.Forms. En una aplicación de consola, agregue <UseWindowsForms>true</UseWindowsForms> al archivo del proyecto
  • Este bucle no termina por sí solo. Para detenerlo, hay que llamar a Application.ExitThread() en ese mismo subproceso (o a Application.Exit() para terminar toda la aplicación). Si en el ejemplo anterior se quiere llegar a CoUninitialize(), hace falta además un mecanismo aparte que avise al subproceso STA de que debe finalizar y que llame a ExitThread

Si no se quiere depender de WinForms, se puede escribir a mano el bucle GetMessage / DispatchMessage de 6.5, o usar MsgWaitForMultipleObjects, que permite esperar tanto mensajes como objetos de sincronización a la vez. Esta segunda opción es la habitual cuando se quiere «recibir el aviso de finalización mediante un evento sin dejar de atender las llamadas de COM».

6.7. Otro ejemplo de bloqueo: callback durante una llamada síncrona

STA no solo implica que «la llamada se reenvía»; según la situación, también puede llegar un callback en sentido inverso (del servidor al cliente). En particular, el patrón en el que se produce un callback durante una llamada síncrona es un caso clásico de deadlock.

Deadlock por un callback durante una llamada síncronaDiagrama de secuencia que muestra cómo el subproceso de UI en STA llama de forma síncrona a DoWork y queda a la espera sin procesar mensajes, mientras el servidor COM intenta entregar un callback que la UI no puede recibir, de modo que ambos quedan esperándose mutuamenteServidor COMSubproceso de UI (STA)Servidor COMSubproceso de UI (STA)Esperando el retorno de DoWork(sin procesar mensajes)Como está esperando,no puede recibir el callbackEsperando a que termine el callbackCada uno espera al otro → deadlockDoWork() (llamada síncrona)ProgressCallback() (callback)

Por qué es fácil que se convierta en un deadlock:

  1. El subproceso de UI hace una llamada síncrona (bloqueante) a DoWork()
  2. El subproceso de UI está esperando el retorno (sin procesar mensajes)
  3. El servidor envía ProgressCallback() al subproceso de UI
  4. Como el subproceso de UI está esperando, no puede recibir el callback
  5. El servidor está esperando a que se complete el callback
  6. Cada uno espera al otro → esto nunca avanza

La duración del procesamiento no tiene relevancia aquí. El propio patrón en el que llega un callback durante una llamada síncrona ya es, de por sí, propenso a causar problemas.

Nota adicional: COM también cuenta con mecanismos para procesar mensajes y reentrar según la situación, y el comportamiento varía según el componente y la forma de la llamada. No siempre termina en deadlock, pero es prudente evitar este patrón.

7. Guía rápida de cuándo usar cada uno

Situación Elección Razón Qué más hace falta
Involucra UI (WinForms / WPF) STA Los controles de UI tienen afinidad con un único subproceso y el subproceso de UI ya tiene un bucle de mensajes (3.1) Nada en particular. Es STA por defecto
Se necesita un gran volumen de procesamiento en paralelo MTA Varios subprocesos comparten un mismo Apartment y pueden llamar directamente sin reenvío (2.2) Diseño seguro para subprocesos (thread-safe) del lado del objeto COM. No es exclusión del lado del llamador, sino responsabilidad de la implementación del objeto
Se quiere usar COM en STA desde segundo plano STA + bucle de mensajes Al recibir llamadas de otro subproceso, hace falta un punto de entrada para el reenvío (6.2) Application.Run() o el bucle GetMessage. También hay que diseñar un mecanismo de finalización (ExitThread, etc.) (6.6)
Los requisitos del componente COM ya están definidos Ajustarse a esos requisitos El Apartment es la propia regla de llamada y no se puede cambiar después (capítulo 5) Compruebe el valor de ThreadingModel. Si es Apartment, diseñe partiendo de STA
Se llama con alta frecuencia Acercarse al Apartment del lado que llama Cada vez que se cruza el límite se interpone el marshalling (2.4) Si resulta difícil, diseñe para reducir el número de llamadas (agrupándolas)

Cuando tenga dudas, resulta más fácil decidir siguiendo el orden de prioridad: «requisitos del componente > presencia de UI > paralelismo». Como el Apartment queda fijado en la primera inicialización y no se puede cambiar después, esto es lo único que conviene decidir antes de empezar la implementación.

8. Resumen

STA/MTA es el modelo de subprocesos de COM: STA toma la forma de 1 subproceso = 1 Apartment, y MTA la de varios subprocesos compartiendo 1 Apartment. Las llamadas que cruzan un Apartment son reenviadas por COM a través de Proxy/Stub (fuera de las interfaces estándar, hace falta generarlo y registrarlo con MIDL u otra herramienta), pero esto conlleva overhead de marshalling, por lo que en escenarios donde se esperan llamadas frecuentes conviene decidir con cuidado el diseño del Apartment.

Desde el punto de vista de los bloqueos, todo se reduce a un único punto: «un subproceso STA que va a recibir llamadas de otro subproceso debe tener una bomba de mensajes en funcionamiento». Llamar a un subproceso STA que no está procesando mensajes tiende a producir un bloqueo, y el patrón en el que llega un callback durante una llamada síncrona también es propenso al deadlock. El subproceso de UI ya cuenta desde el principio con «afinidad con un único subproceso» y con un «bucle de mensajes», por lo que cumple este requisito sin implementación adicional, y de ahí que encaje bien con los objetos COM en STA.

9. Referencias

  • 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 (motivo por el que el bucle de mensajes es obligatorio, la ventana oculta, la advertencia de deadlock en .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 (el valor de ThreadingModel) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32
  • Método Application.Run (la forma de ejecutar el bucle de mensajes sin un formulario y cómo detenerlo) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
  • MsgWaitForMultipleObjects (esperar simultáneamente mensajes y objetos de sincronización) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects

Descargar el archivo Word de este artículo

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Debo elegir STA o MTA?
La distinción básica es: STA para procesos relacionados con la interfaz de usuario y MTA para un gran volumen de procesamiento en paralelo. STA tiene una alta afinidad con el subproceso, ya que asigna un Apartment por subproceso, pero tiende a congestionarse cuando hay muchos llamadores. MTA comparte un único Apartment entre varios subprocesos, lo que ofrece mayor paralelismo, pero exige que el objeto COM tenga un diseño seguro para subprocesos (thread-safe). Si no se aplica ninguno de los dos casos, lo más realista es ajustarse a los requisitos de la biblioteca existente o del servidor COM que se vaya a utilizar.
¿Por qué el subproceso de UI usa STA?
Porque el diseño del subproceso de UI y el de STA coinciden. Los controles de UI, como los botones o los cuadros de texto, no son seguros para subprocesos y solo se pueden manipular de forma segura desde el subproceso que los creó. STA es, del mismo modo, un modelo de afinidad con un único subproceso. Además, el subproceso de UI siempre ejecuta un bucle de mensajes para procesar los eventos de ventana, lo que cumple sin implementación adicional el requisito de STA de contar con una bomba de mensajes (message pump). Por eso el subproceso de UI de WinForms/WPF usa STA de forma predeterminada.
¿Por qué se produce un bloqueo (hang) al llamar a un objeto COM en STA?
Las llamadas a un objeto COM en STA se procesan en el subproceso STA que lo creó. Las llamadas desde otro subproceso son reenviadas por COM a través de mensajes/RPC, pero si el subproceso STA no está ejecutando un bucle de mensajes, no puede recibir ese reenvío y el llamador se queda esperando indefinidamente, lo que produce un bloqueo. Para evitarlo, hay que ejecutar un bucle de mensajes en el subproceso STA al que se llama desde otro subproceso, crear y usar el objeto en el subproceso de UI, o bien usar MTA desde el principio si no se necesita STA.
¿Para qué sirve el atributo [STAThread] de .NET?
Es un envoltorio (wrapper) para configurar el Apartment Model de COM. Al aplicarlo al método Main, ese subproceso se inicializa como STA en el momento en que se usa COM. Sin embargo, no se inicializa hasta que realmente se llama a COM, por lo que no tiene efecto en aplicaciones que no lo usan. Tampoco afecta a los subprocesos adicionales que se creen, así que hay que configurarlos con Thread.SetApartmentState antes de iniciarlos. También hay que tener en cuenta que el Apartment queda fijado en la primera inicialización y no se puede cambiar después.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog