¿Qué operaciones de locking hacen que un hilo STA bombee mensajes COM?

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

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

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

Listas parciales hasta el momento:

Operaciones de locking que bombean *:

  • Thread.Join
  • WaitHandle.WaitOne / WaitAny / WaitAll ( WaitAll no se puede llamar desde un hilo STA)
  • GC.WaitForPendingFinalizers
  • Monitor.Enter (y, por lo tanto, lock ) – bajo algunas condiciones
  • ReaderWriterLock
  • BlockingCollection

Operaciones de locking que no bombean

  • Thread.Sleep
  • Console.ReadKey (léelo en alguna parte)

* Tenga en cuenta la respuesta de Noseratio que dice que incluso las operaciones que sí lo hacen, lo hacen para un conjunto muy limitado de mensajes específicos de COM no revelados.

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

StaTaskScheduler y STA mensaje de bombeo de mensajes

Sin embargo, bombeará un conjunto muy limitado no revelado de mensajes COM-específicos , al igual que las otras API que enumeró. No generará 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 ciclo de mensajes con todas las funciones.

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

¿En qué medida el comportamiento de bombeo estándar de WaitOne es realmente limitado? A continuación se muestra un ejemplo típico que causa un interlocking en el hilo 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 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á un lapso de tiempo de ~ 4000 ms, aunque la tarea tarda solo 2000 ms en completarse.

Eso sucede porque la await callback de continuación se progtwig a través de WindowsFormsSynchronizationContext.Post , que utiliza Control.BeginInvoke , que a su vez usa PostMessage , publicando un mensaje regular de Windows registrado con RegisterWindowMessage . Este mensaje no se bombea ni handle.WaitOne . handle.WaitOne vez.

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

Ahora implementemos una versión de WaitOne con extracción explícita (y llámenos 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 cambie 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 el mensaje de continuación de await se bombea por Application.DoEvents() , la tarea finaliza 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 muy pocos casos específicos). Es una fuente de varios problemas, como la reentrada UI. 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, lo que es vital para el cálculo de COM.

Cómo se divulga realmente el bombeo. Hay llamadas internas al .NET runtime que a su vez usan CoWaitForMultipleHandles para realizar la espera en los hilos STA. La documentación para esa API es bastante deficiente, pero leer algunos libros COM y el código fuente de Wine podría darte algunas ideas aproximadas.

Internamente, llama a MsgWaitForMultipleObjectsEx con QS_SENDMESSAGE | QS_ALLPOSTMESSAGE | Indicadores QS_PAINT. Analicemos 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 una mala idea bloquear los manipuladores de pintura porque es probable que entren en un bucle reentrante y provoquen un desbordamiento de la stack.

QS_SENDMESSAGE es para mensajes enviados desde otros hilos 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 mensajes UI del Explorador y el Administrador de tareas, por lo que envía un mensaje WM_CLOSE (clic derecho en una aplicación que no responde en la barra de tareas y selecciona Cerrar), mensajes de icono de bandeja y posiblemente algo más (WM_ENDSESSION )

QS_ALLPOSTMESSAGE es para el rest. Los mensajes en realidad se filtran, por lo que solo se procesan los mensajes de la ventana del departamento oculto y los mensajes DDE (WM_DDE_FIRST – WM_DDE_LAST).

Recientemente aprendí de la manera más difícil que Process.Start puede funcionar. No esperé el proceso ni le pedí su pid, solo quería que corriera al costado.

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

Si bien todo el bombeo de STA es sorprendente, ¡descubrí que este es muy sorprendente, por decir lo menos!

Intereting Posts