multithreading delphi delphi-xe2

multithreading - La aplicación se cuelga en SysUtils-> DoneMonitorSupport en la salida



delphi delphi-xe2 (4)

En Delphi XE5, Embarcadero resolvió esto agregando (Now - Start > 1 / MSecsPerDay) or al ciclo de repeat until en CleanEventList para que se rinda después de 1 milisegundo. A continuación, elimina el evento independientemente de si el Lock era 0 .

Estoy escribiendo una aplicación de uso intensivo de subprocesos que se cuelga cuando sale.

He rastreado las unidades del sistema y he encontrado el lugar donde el programa entra en un bucle infinito. Está en la línea SysUtils 19868 -> DoneMonitorSupport -> CleanEventList :

repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;

Busqué una solución en línea y encontré un par de informes de control de calidad:

Desafortunadamente, esto no parece estar relacionado con mi situación, ya que no uso ni TThreadList ni TMonitor .

Estoy bastante seguro de que todos mis hilos han finalizado y se han destruido, ya que todos se heredan de un hilo base que mantiene una cuenta de creación / destrucción.

¿Alguien se ha encontrado con un comportamiento similar antes? ¿Conoce alguna estrategia para descubrir dónde puede estar la causa raíz?


He estado observando cómo se implementan los bloqueos TMonitor , y finalmente hice un descubrimiento interesante. Para un poco de drama, primero te diré cómo funcionan las cerraduras.

Cuando llama a cualquier función de TMonitor en un TObject , se crea una nueva instancia del registro de TMonitor y esa instancia se asigna a un MonitorFld dentro del propio objeto. Esta asignación se realiza de forma segura para subprocesos, utilizando InterlockedCompareExchangePointer . Debido a este truco, TObject solo contiene una cantidad de datos de tamaño de puntero para el soporte de TMonitor , no contiene la estructura completa de TMonitor. Y eso es algo bueno.

Esta estructura de TMonitor contiene una serie de registros. Comenzaremos con el campo FLockCount: Integer . Cuando el primer hilo usa TMonitor.Enter() en cualquier objeto, este campo combinado de contador de bloqueo tendrá el valor CERO. Nuevamente, utilizando un método InterlockedCompareExchange , se adquiere el bloqueo y se inicia el contador. No habrá bloqueo para el subproceso de llamada, no habrá cambio de contexto ya que todo esto se realiza en proceso.

Cuando el segundo hilo intenta TMonitor.Enter() el mismo objeto, el primer intento de bloqueo fallará. Cuando eso sucede, Delphi sigue dos estrategias:

  • Si el desarrollador usó TMonitor.SetSpinCount() para establecer un número de "giros", Delphi hará un ciclo de espera de ocupado, girando el número dado de veces. Eso es muy bueno para bloqueos pequeños porque permite adquirir el bloqueo sin hacer un cambio de contexto.
  • Si el recuento de giros expira (o no hay recuento de giros, y de forma predeterminada el conteo de giros cero), TMonitor.Enter() iniciará una Espera en el evento devuelto por TMonitor.GetEvent() . En otras palabras, no estará ocupado, espere desperdiciando ciclos de CPU. Recuerde el TMonitor.GetEvent() porque eso es muy importante.

Digamos que tenemos un subproceso que adquirió el bloqueo y un subproceso que intentó adquirir el bloqueo pero ahora está esperando el evento devuelto por TMonitor.GetEvent . Cuando el primer hilo llame a TMonitor.Exit() notará (a través del campo FLockCount ) que hay al menos otro bloqueo de hilo. Por lo tanto, pulsa inmediatamente lo que normalmente debería ser el evento asignado previamente (llama a TMonitor.GetEvent() ). Pero como los dos subprocesos, el que llama a TMonitor.Exit() y el que llamó a TMonitor.Enter() , en realidad podrían llamar a TMonitor.GetEvent() al mismo tiempo, hay un par de trucos más dentro de TMonitor.GetEvent() para asegurarse de que solo se asigne un evento, independientemente del orden de las operaciones.

Para unos momentos más divertidos, ahora profundizaremos en la forma en que funciona TMonitor.GetEvent() . Esta cosa vive dentro de la unidad del System (ya sabes, la que no podemos compilar para jugar), pero resulta que delega el deber de asignar el Evento a otra unidad, a través del puntero System.MonitorSupport . Eso apunta a un registro de tipo TMonitorSupport que declara 5 punteros de función:

  • NewSyncObject : asigna un nuevo evento para fines de sincronización
  • FreeSyncObject : desasigna el evento asignado para fines de sincronización
  • NewWaitObject : asigna un nuevo evento para operaciones de espera
  • FreeWaitObject - desasigna ese evento de espera
  • WaitAndOrSignalObject - bueno .. espera o señala.

También resulta que los objetos devueltos por las funciones NewXYZ podrían ser cualquier cosa, porque solo se usan para la llamada a WaitXYZ y para la llamada correspondiente a FreeXyzObject . La forma en que se implementan esas funciones en SysUtils está diseñada para proporcionar a esos bloqueos una cantidad mínima de bloqueo y cambio de contexto; Debido a eso, los objetos en sí mismos (devueltos por NewSyncObject y NewWaitObject ) no son directamente los eventos devueltos por CreateEvent() , sino punteros a los registros en SyncEventCacheArray . Va aún más lejos, los eventos reales de Windows no se crean hasta que sea necesario. Debido a eso, los registros en SyncEventCacheArray contienen un par de registros:

  • TSyncEventItem.Lock : esto le dice a Delphi que prefiere que Lock se esté usando para algo ahora o no y
  • TSyncEventItem.Event : esto contiene el Evento real que se usará para la sincronización, si se requiere la espera.

Cuando la aplicación finaliza, SysUtils.DoneMonitorSupport recorre todos los registros en SyncEventCacheArray y espera que el bloqueo se convierta en CERO, es decir, espera que el bloqueo deje de ser utilizado por cualquier cosa. Teóricamente, siempre que ese bloqueo NO sea cero, al menos un subproceso podría estar utilizando el bloqueo, por lo que lo más sensato sería esperar para no causar errores de AccessViolations. Y finalmente llegamos a nuestra pregunta actual: SysUtils.DoneMonitorSupport in SysUtils.DoneMonitorSupport

¿Por qué una aplicación podría bloquearse en SysUtils.DoneMonitorSupport incluso si todos los subprocesos terminaron correctamente?

Debido a que al menos un Evento asignado usando uno de NewSyncObject o NewWaitObject no fue liberado usando su FreeSyncObject o FreeWaitObject correspondiente. Y volvemos a la rutina TMonitor.GetEvent() . El evento que asigna se guarda en el registro de TMonitor que corresponde al objeto que se utilizó para TMonitor.Enter() . El puntero a ese registro solo se mantiene en los datos de instancia de ese objeto, y se mantiene allí durante la vida de la aplicación. Buscando el nombre del campo, FLockEvent , lo encontramos en el archivo System.pas :

procedure TMonitor.Destroy; begin if (MonitorSupport <> nil) and (FLockEvent <> nil) then MonitorSupport.FreeSyncObject(FLockEvent); Dispose(@Self); end;

y una llamada a ese destructor de registros aquí: procedure TObject.CleanupInstance .

En otras palabras, el evento de sincronización final solo se libera cuando se libera el objeto que se utilizó para la sincronización.

Responda a la pregunta de OP:

La aplicación se bloquea porque no se liberó al menos un OBJETO que se usó para TMonitor.Enter() .

Soluciones posibles:

Lamentablemente no me gusta esto. No está bien, me refiero a que la penalización por no liberar un objeto pequeño debería ser una pequeña pérdida de memoria, ¡no una aplicación que cuelga! Esto es especialmente malo para las aplicaciones de servicio en las que un servicio puede simplemente colgarse para siempre, no cerrarse completamente pero no puede responder a ninguna solicitud.

¿Las soluciones para el equipo de Delphi? NO deben colgarse en el código de finalización de la unidad SysUtils , no importa qué. Probablemente deberían ignorar el Lock y moverse para cerrar el controlador de eventos. En esa etapa (finalización de la unidad SysUtils), si todavía hay código ejecutándose en algún subproceso, está en muy mal estado ya que la mayoría de las unidades se finalizaron, no se está ejecutando en el entorno para el que fue diseñado.

¿Para los usuarios de Delphi? Podemos reemplazar el MonitorSupport con nuestra propia versión, una que no hace esas pruebas exhaustivas en el momento de la finalización.


He trabajado alrededor del error de la siguiente manera:

Copie System.SysUtils , InterlockedAPIs.inc y EncodingData.inc a mi directorio de aplicación y modifique el siguiente código en System.SysUtils :

procedure CleanEventList(var EventCache: array of TSyncEventItem); var I: Integer; begin for I := Low(EventCache) to High(EventCache) do begin if InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0 then DeleteSyncWaitObj(EventCache[I].Event); //repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0; //DeleteSyncWaitObj(EventCache[I].Event); end; end;

También agregué esta comprobación en la parte superior de System.SysUtils para recordarme que actualice el archivo System.SysUtils si cambio las versiones de Delphi:

{$IFNDEF VER230} !!!!!!!!!!!!!!!! You need to update this unit to fix the bug at line 19868 See http://.com/questions/14217735/application-hangs-in-sysutils-donemonitorsupport-on-exit !!!!!!!!!!!!!!!! {$ENDIF}

Después de estos cambios mi aplicación se apaga correctamente.

Nota: Intenté agregar "ReportMemoryLeaksOnShutdown" como sugirió LU RD, pero al apagar mi aplicación ingresó en una condición de carrera apareciendo numerosos diálogos de error de tiempo de ejecución. Algo similar sucede cuando intento la funcionalidad de pérdida de memoria de EurekaLog.


Podría reproducir su problema usando el ejemplo provisto por Cosmin. También podría resolver el problema simplemente liberando SyncObj después de que se hayan completado todos los hilos.

Como no tengo acceso a su código, no puedo decir más, pero probablemente no se libere alguna instancia de objeto utilizada por TMonitor.