Conocimientos básicos de STA/MTA en COM - El modelo de subprocesos y cómo evitar los bloqueos
· Actualizado el: · Go Komura · 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
- 1. Primero, la conclusión (en pocas palabras)
- 2. Patrones de llamada del Apartment Model (diagrama)
- 3. STA (Single-Threaded Apartment)
- 4. MTA (Multi-Threaded Apartment)
- 5. Dónde se determina STA/MTA
- 6. Ejemplo concreto de bloqueo por un mal uso de STA
- 6.1. Situación habitual
- 6.2. Qué ocurre
- 6.3. Pseudocódigo (patrón de fallo típico)
- 6.4. Puntos clave para evitarlo
- 6.5. ¿Qué significa realmente «ejecutar el bucle de mensajes»?
- 6.6. Ejemplo del enfoque correcto (a grandes rasgos, así)
- 6.7. Otro ejemplo de bloqueo: callback durante una llamada síncrona
- 7. Guía rápida de cuándo usar cada uno
- 8. Resumen
- 9. Referencias
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.
flowchart LR
accTitle: Llamada directa dentro del mismo subproceso STA
accDescr: Diagrama que muestra que, dentro de un mismo subproceso STA, el código llamador invoca directamente al objeto COM sin pasar por marshalling
subgraph STA[Subproceso STA]
Caller[Código llamador]
Obj[Objeto COM]
Caller -->|Llamada directa| Obj
end
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).
flowchart LR
accTitle: Llamada dentro del mismo MTA
accDescr: Diagrama que muestra que varios subprocesos de trabajo dentro de un mismo MTA pueden llamar directamente al mismo objeto COM
subgraph MTA[MTA(un único Apartment)]
Thread1[Subproceso de trabajo 1]
Thread2[Subproceso de trabajo 2]
Obj[Objeto COM]
Thread1 -->|Llamada directa| Obj
Thread2 -->|Llamada directa| Obj
end
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.
flowchart LR
accTitle: Llamada que cruza Apartments a través de Proxy/Stub
accDescr: Diagrama 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 MTA
subgraph STA[Subproceso STA]
StaCaller[Código llamador]
end
subgraph RT[Runtime de COM(automático)]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[Subproceso MTA]
MtaObj[Objeto COM]
end
StaCaller -->|Llamada| Proxy
Stub -->|Reenvío| MtaObj
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 MTAThread.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 usaThread.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.
sequenceDiagram
accTitle: Secuencia de bloqueo en el ejemplo de fallo de STA
accDescr: Diagrama 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 bloqueo
participant Main as Subproceso principal
participant STA as Subproceso STA
participant COM as Runtime de COM
Main->>STA: Inicio del subproceso
STA->>STA: CoInitializeEx (STA)
STA->>STA: Creación del objeto COM
STA->>Main: ready.Set()
STA->>STA: Espera en done.WaitOne()
Note over STA: Sin bucle de mensajes<br/>bloqueado aquí
Main->>COM: CallComObject()
COM->>STA: Intenta reenviar la llamada
Note over COM: Reenvía mediante un mensaje, pero...
Note over STA: Como está en WaitOne,<br/>no puede procesar el mensaje
Note over Main: El llamador también sigue esperando
Note over Main,STA: Ambos en espera → bloqueo
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 aApplication.Exit()para terminar toda la aplicación). Si en el ejemplo anterior se quiere llegar aCoUninitialize(), hace falta además un mecanismo aparte que avise al subproceso STA de que debe finalizar y que llame aExitThread
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.
sequenceDiagram
accTitle: Deadlock por un callback durante una llamada síncrona
accDescr: Diagrama 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 mutuamente
participant UI as Subproceso de UI (STA)
participant Server as Servidor COM
UI->>Server: DoWork() (llamada síncrona)
Note over UI: Esperando el retorno de DoWork<br/>(sin procesar mensajes)
Server->>UI: ProgressCallback() (callback)
Note over UI: Como está esperando,<br/>no puede recibir el callback
Note over Server: Esperando a que termine el callback
Note over UI,Server: Cada uno espera al otro → deadlock
Por qué es fácil que se convierta en un deadlock:
- El subproceso de UI hace una llamada síncrona (bloqueante) a
DoWork() - El subproceso de UI está esperando el retorno (sin procesar mensajes)
- El servidor envía
ProgressCallback()al subproceso de UI - Como el subproceso de UI está esperando, no puede recibir el callback
- El servidor está esperando a que se complete el callback
- 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
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
Trampas de registro y bitness en el desarrollo COM/OCX/ActiveX
Un repaso práctico de los problemas más comunes en COM, OCX y ActiveX: bitness de 32/64 bits, Visual Studio 2022, regsvr32/Regasm, permis...
¿Qué es Reg-Free COM? - El mecanismo para usar COM sin registro
Explicamos los fundamentos de Reg-Free COM, el papel del contexto de activación y los manifiestos, sus ventajas, límites y criterios de d...
Qué son COM, ActiveX y OCX - Diferencias y relación explicadas
Explicamos qué son COM, ActiveX y OCX: diferencias, relación, vínculo con OLE, dónde se usan y cómo abordarlos hoy desde una perspectiva ...
Introducción a Media Foundation - Cómo entender la API desde la perspectiva de COM
Explicamos qué es Media Foundation junto con los términos básicos de su API multimedia en Windows —COM, HRESULT, IMFSourceReader, MFT— en...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Migración de ActiveX
Decisiones para conservar, encapsular o sustituir componentes COM / ActiveX / OCX.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Consultoría técnica y revisión de diseño
Ordenar STA/MTA, el bucle de mensajes y la marshalling está directamente relacionado con la división de responsabilidades y la revisión de los límites entre subprocesos antes de la implementación.
Reutilización y migración de activos existentes
Es una base difícil de evitar al trabajar con activos existentes que incluyen COM, por lo que también encaja bien con consultas sobre aprovechamiento y migración de activos existentes.
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.