c# com interop message-pump

c# - ¿Qué operaciones de bloqueo hacen que un subproceso STA bombee mensajes COM?



interop message-pump (3)

Cuando se crea una instancia de un objeto COM en un subproceso STA, el subproceso generalmente tiene que implementar un bombeo de mensajes para ordenar las llamadas hacia y desde otros subprocesos (consulte here ).

Uno puede bombear mensajes manualmente o confiar en el hecho de que algunas, pero no todas , las operaciones de bloqueo de subprocesos bombearán automáticamente mensajes relacionados con COM mientras esperan. La documentación a menudo no ayuda a decidir cuál es cuál (consulte esta pregunta relacionada ).

¿Cómo puedo determinar si una operación de bloqueo de subprocesos bombeará mensajes COM en una STA?

Listas parciales hasta el momento:

Operaciones de bloqueo que bombean *:

Operaciones de bloqueo que no bombean:

* Note BlockingCollection que dice que incluso las operaciones que bombean, lo hacen para un conjunto muy limitado no revelado de mensajes específicos de COM.


¿Cómo funciona el bombeo en realidad se revela. Hay llamadas internas al tiempo de ejecución de .NET que, a su vez, utiliza CoWaitForMultipleHandles para realizar la espera en los subprocesos STA. La documentación para esa API es bastante escasa, pero leer algunos libros COM y el código fuente de Wine podría darle algunas ideas aproximadas.

Internamente llama a MsgWaitForMultipleObjectsEx con el QS_SENDMESSAGE | QS_ALLPOSTMESSAGE | Banderas QS_PAINT. Vamos a analizar para qué se usa cada uno.

QS_PAINT es el más obvio, los mensajes WM_PAINT se procesan en la bomba de mensajes. Por lo tanto, es realmente una mala idea hacer un bloqueo en los controladores de pintura porque probablemente entrará en el bucle de reingreso y provocará un desbordamiento de pila.

QS_SENDMESSAGE es para mensajes enviados desde otros subprocesos y aplicaciones. Esta es en realidad una forma de cómo funciona la comunicación entre procesos. La parte fea es que también se usa para los mensajes de UI de Explorer y Task Manager, por lo que bombea el mensaje WM_CLOSE (haga clic con el botón derecho en una aplicación que no responde en la barra de tareas y seleccione Cerrar), los mensajes del ícono de la bandeja y posiblemente otra cosa (WM_ENDSESSION ).

QS_ALLPOSTMESSAGE es para el resto. En realidad, los mensajes se filtran, por lo que solo se procesan los mensajes de la ventana de apartamento oculta y los mensajes DDE (WM_DDE_FIRST - WM_DDE_LAST).


Recientemente aprendí la manera difícil en que Process.Start puede bombear. No esperé el proceso ni le pregunté a su pid, solo quería que se ejecutara al lado.

En las pilas de llamadas (no tengo a mano) lo vi entrar en el código específico de ShellInvoke, por lo que esto solo podría aplicarse a ShellInvoke = true.

Si bien todo el bombeo de STA es bastante sorprendente, encontré que esta es muy sorprendente, por decir lo menos.


BlockingCollection efectivamente bombeará mientras bloquea. Aprendí que al responder a la siguiente pregunta, que tiene algunos detalles interesantes sobre el bombeo de STA:

StaTaskScheduler y bombeo de mensajes del hilo STA

Sin embargo, bombeará un conjunto muy limitado no revelado de mensajes específicos de COM , al igual que las otras API enumeradas. No bombeará mensajes Win32 de propósito general (un caso especial es WM_TIMER , que tampoco se enviará). Esto podría ser un problema para algunos objetos STA COM que esperan un bucle de mensajes con todas las funciones.

Si desea experimentar con esto, cree su propia versión de SynchronizationContext , SetWaitNotificationRequired SynchronizationContext.Wait , llame a SetWaitNotificationRequired e instale su objeto de contexto de sincronización personalizado en un hilo STA. A continuación, establezca un punto de interrupción dentro de Wait y vea qué API hará que se llame.

¿Hasta qué punto el comportamiento de bombeo estándar de WaitOne es realmente limitado? A continuación se muestra un ejemplo típico que provoca un interbloqueo en el subproceso de la interfaz de usuario. Yo uso WinForms aquí, pero la misma preocupación se aplica a WPF:

public partial class MainForm : Form { public MainForm() { InitializeComponent(); this.Load += (s, e) => { Func<Task> doAsync = async () => { await Task.Delay(2000); }; var task = doAsync(); var handle = ((IAsyncResult)task).AsyncWaitHandle; var startTick = Environment.TickCount; handle.WaitOne(4000); MessageBox.Show("Lapse: " + (Environment.TickCount - startTick)); }; } }

El cuadro de mensaje mostrará el lapso de tiempo de ~ 4000 ms, aunque la tarea tarda solo 2000 ms en completarse.

Esto sucede porque la devolución de llamada de continuación de await está programada a través de WindowsFormsSynchronizationContext.Post , que usa Control.BeginInvoke , que a su vez usa PostMessage , publicando un mensaje regular de Windows registrado con RegisterWindowMessage . Este mensaje no se bombea y se handle.WaitOne . handle.WaitOne tiempos de espera.

Si utilizamos handle.WaitOne(Timeout.Infinite) , tendríamos un interbloqueo clásico.

Ahora implementemos una versión de WaitOne con bombeo explícito (y llamémoslo WaitOneAndPump ):

public static bool WaitOneAndPump( this WaitHandle handle, int millisecondsTimeout) { var startTick = Environment.TickCount; var handles = new[] { handle.SafeWaitHandle.DangerousGetHandle() }; while (true) { // wait for the handle or a message var timeout = (uint)(Timeout.Infinite == millisecondsTimeout ? Timeout.Infinite : Math.Max(0, millisecondsTimeout + startTick - Environment.TickCount)); var result = MsgWaitForMultipleObjectsEx( 1, handles, timeout, QS_ALLINPUT, MWMO_INPUTAVAILABLE); if (result == WAIT_OBJECT_0) return true; // handle signalled else if (result == WAIT_TIMEOUT) return false; // timed-out else if (result == WAIT_ABANDONED_0) throw new AbandonedMutexException(-1, handle); else if (result != WAIT_OBJECT_0 + 1) throw new InvalidOperationException(); else { // a message is pending if (timeout == 0) return false; // timed-out else { // do the pumping Application.DoEvents(); // no more messages, raise Idle event Application.RaiseIdle(EventArgs.Empty); } } } }

Y cambia el código original de esta manera:

var startTick = Environment.TickCount; handle.WaitOneAndPump(4000); MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));

El lapso de tiempo ahora será ~ 2000 ms, porque Application.DoEvents() bombea el mensaje de continuación de await , la tarea se completa y se señala su identificador.

Dicho esto, nunca recomendaría usar algo como WaitOneAndPump para el código de producción (además de para muy pocos casos específicos). Es una fuente de varios problemas como la re-entrada de la interfaz de usuario. Esos problemas son la razón por la que Microsoft ha limitado el comportamiento de bombeo estándar a solo ciertos mensajes específicos de COM, vitales para el cálculo de referencias de COM.