Ejecutando VCL en un hilo separado

Ahora tengo una situación bastante rara. Tengo una aplicación que interactúa directamente con la cola de mensajes de Windows. Esta aplicación también ejecuta scripts Lua externos con LuaJIT. Quería tener una función de depuración para estos scripts, así que creé una aplicación VCL simple y luego la convertí en una biblioteca DLL. Cuando la primera aplicación inicia una sesión de depuración con la biblioteca, esta DLL crea un subproceso separado, donde se inicializa y ejecuta toda la instalación de VCL.

procedure TDebuggerThread.Execute;
begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm (TMainForm, MainForm);
  Application.Run;
end;

¿VCL es totalmente compatible con la ejecución de esta manera? ¿A qué hilo se TThread.Synchronize (Proc: TThreadProc) enviar su mensaje?

Inb4 "los mensajes a VCL y a la aplicación principal se desordenarán", no lo harán porque cada hilo tiene su propia cola de mensajes.

Además, puede ver las fuentes aquí. (Tal vez) se nombra la biblioteca problemática LuaDebugger. En lugar de un cliente adecuado (Core, Engine, Client) que estoy usando actualmente LuaDefaultHost, que es una aplicación de consola bastante simple, que solicita el depurador y se comporta principalmente como lua.exe. Con el cliente de la consola, el depurador funciona sorprendentemente bien: el único problema que he encontrado es que si cierro la ventana de la consola mientras la biblioteca todavía está en uso, VCL arroja "El controlador de ventana ya no es válido" (en ruso :/). Si dejo que el cliente termine de interactuar con el depurador de la forma en que se supone que debe hacerlo, todo sale bien. Probablemente llamando Windows.TerminateThread durante la finalización de la unidad debería arreglar eso.

preguntado el 21 de septiembre de 13 a las 12:09

O me estoy perdiendo algo o está accediendo y ejecutando métodos y ejecutando una cola de mensajes en un objeto de aplicación que se crea en un hilo diferente. Creo que debería haberse estrellado antes... -

@SertacAkyuz Yo también pensé de esa manera. Aparentemente, VCL es más flexible de lo que considerábamos. :O Sin embargo, todavía no intenté usar la biblioteca con un cliente GUI, solo con uno de consola. Probablemente necesito ejecutar un experimento especial para determinar si VCL todavía usa la cola del subproceso principal de alguna manera. -

@Delfgamer ¿Cómo podría hacer eso la VCL? ¿Cómo podría la VCL forzarse a sí misma en un subproceso propiedad del ejecutable? No puede hacer eso por la fuerza. Requiere la cooperación del ejecutable. La VCL no tiene ningún concepto del hilo principal del proceso. Solo existe el subproceso VCL, ese subproceso que inicializó el VCL. -

@DavidHeffernan Unidades' inicialización el código se ejecuta en el hilo del cliente, Application.* los métodos se ejecutan en el subproceso del depurador. Supongo que los recursos creados por la inicialización de la unidad no están vinculados a un hilo específico. Parece que sí, pero puedo estar equivocado. Antes dijiste que solo funcionaría si llamaba LoadLibrary de un hilo nuevo; ahora dices mudarte Application.Initialize alrededor está bien. ¿Qué significa? -

No estoy diciendo nada diferente. El código de inicialización se ejecuta desde DllMain. Eso determina el hilo VCL. Ese es el subproceso que llama a LoadLibrary ya que DllMain se ejecuta en el subproceso que llama a LoadLibrary. Ergo, el subproceso utilizado por la VCL es el que llama a LoadLibrary en el host. No puedes cambiar eso. -

3 Respuestas

Su única esperanza es crear el hilo y luego cargar la DLL desde ese hilo. Entonces, para ser lo más claro posible, crea el hilo y luego, desde el código que se ejecuta dentro de ese hilo, llama LoadLibrary para cargar la DLL.

La VCL debe ejecutarse fuera del subproceso que carga la DLL. La inicialización de VCL ocurre durante la inicialización de la DLL y eso determina qué subproceso es el subproceso principal de VCL. El subproceso principal de VCL es el subproceso que inicializa la VCL, el subproceso que carga la DLL.

Es probable que tenga que mantener la mente despejada con todo este enfoque porque tendrá dos subprocesos de GUI, dos bombas de mensajes, en un solo proceso. Mostrar una ventana modal implica deshabilitar las ventanas en ambos subprocesos de la GUI.

No puedo estar seguro de que este enfoque general (dos subprocesos GUI en el mismo proceso, uno de los cuales es un subproceso VCL) funcione, sin haberlo hecho nunca. Sin embargo, creo que hay una buena posibilidad de que vuele.


También haces una pregunta bastante específica:

¿A qué subproceso TThread.Synchronize (Proc: TThreadProc) enviará su mensaje?

La respuesta es siempre el hilo que inicializó el módulo. Entonces, para un ejecutable, este es el hilo principal del proceso. Para una DLL, el subproceso que inicializó el módulo es el subproceso que llamó LoadLibrary, el hilo que ejecuta la llamada inicial a DllMain, el hilo que ejecuta el código de inicialización de las unidades DLL. Esto se conoce en RTL/VCL como el hilo principal del módulo. Es el hilo cuyo ID viene dado por System.MainThreadID.

Para probar el punto, en caso de que no confíe en mi palabra, he aquí una pequeña demostración.

Ejecutable

program DllThreading;

{$APPTYPE CONSOLE}

uses
  Classes, Windows;

type
  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure TMyThread.Execute;
var
  lib: HMODULE;
  proc: procedure; stdcall;
begin
  lib := LoadLibrary('dll.dll');
  proc := GetProcAddress(lib, 'foo');
  proc();
  Sleep(INFINITE);
end;

begin
  Writeln('This is the process main thread: ', GetCurrentThreadId);
  TMyThread.Create;
  Readln;
end.

DLL

library Dll;

uses
  Classes, Windows;

type
  TMyThread = class(TThread)
  private
    procedure DoStuff;
  protected
    procedure Execute; override;
  end;

procedure TMyThread.DoStuff;
begin
  Writeln('This is the thread which executes synchronized methods in the DLL: ', GetCurrentThreadId);
end;

procedure TMyThread.Execute;
begin
  Writeln('This is the thread created in the DLL: ', GetCurrentThreadId);
  Synchronize(DoStuff);
end;

procedure foo; stdcall;
begin
  TMyThread.Create;
  CheckSynchronize(1000);
end;

exports
  foo;

begin
  Writeln('This is the initialization thread of the DLL: ', GetCurrentThreadId);
end.

Salida

Este es el hilo principal del proceso: 2788 Este es el hilo de inicialización de la DLL: 5752 Este es el hilo creado en la DLL: 6232 Este es el hilo que ejecuta métodos sincronizados en la DLL: 5752

Respondido el 25 de Septiembre de 13 a las 20:09

En caso de que necesite más elaboración, tan pronto como escriba Application:=.. (usa 'formularios') ya tiene una instancia de Aplicación. Incluir 'formularios' en la cláusula de usos, extrae 'gráficos', 'controles', etc., lo que configura la VCL en sus secciones de inicialización que se llama al inicio de Dll. Observe que no hay 'Aplicación: =' en el código fuente del proyecto de una aplicación de formularios VCL. - Sertac Akyuz

No veo ningún problema inmediato. Sin embargo, tampoco habría pensado en el problema de la modalidad hasta que lo enfrente.. - Sertac Akyuz

Ah, mi culpa, escribí la cita de esta pregunta de memoria y cometí un error. El contenido real se corta y se pega en Execute del archivo principal .dpr. Arreglaré la pregunta ahora. - Delfígamer

Realmente no entiendo ese último comentario. - David Heffernan

No es necesario. Acabo de decir que he corregido un error tipográfico. Centrémonos ahora en el problema de los hilos. :| - Delfígamer

Respuesta de EDN por Remy Lebeau:

La DLL tiene su propia copia independiente de VCL y RTL que están separadas de la copia de la aplicación principal. En su mayor parte, este tipo de uso de subprocesos dentro de la DLL generalmente está bien, pero la funcionalidad sensible al subproceso principal, como TThread.Synchronize() y TThread.Queue(), no funcionará a menos que actualice manualmente el System.MainThreadID variable con la ThreadID de su hilo "principal", a menos que su hilo llame CheckSynchronize() periódicamente (que normalmente se llama automáticamente cuando TThread "despierta" el hilo "principal" cuando un Synchronize/Queue se realiza la operación).

No sé si ajustar System.MainThreadID manualmente es seguro, pero la respuesta a la pregunta principal aquí es "generalmente bien".

Respondido el 26 de Septiembre de 13 a las 03:09

Oh genial, estoy respondiendo mi propia pregunta.

¿Entonces

¿VCL es totalmente compatible con la ejecución de esta manera?

Parece que sí. Por lo que tenemos aquí, el código VCL simplemente se ejecuta en el subproceso "actual" y no sabe si hay otros o si este subproceso "actual" es el principal del proceso o se crea directamente en el mismo binario. Mientras este hilo no se meta con otros, todo va bien.

¿A qué subproceso TThread.Synchronize (Proc: TThreadProc) enviará su mensaje?

El experimento dice que el mensaje se enviará al hilo principal del proceso, no al hilo VCL (naturalmente, es uno donde Application.Run obras). Para sincronizar con el hilo VCL, tengo que enviar mensajes explícitamente a un formulario VCL, aquí (LuaDebugger/USyncForm.pas) es UM_MethodCall personalizado con WParam sosteniendo el puntero al código sincronizado y LParam sosteniendo opcional void* argumento.

Respondido el 25 de Septiembre de 13 a las 19:09

El código VCL simplemente se ejecuta en el hilo "actual". ¡¿Bueno, que esperabas?! Llama Application.Run ¡y espere que el código migre mágicamente a algún otro hilo! Como para Synchronize, el código que estoy viendo es bastante claro. Ejecuta el código en el subproceso VCL, el subproceso que inicializó el módulo. Ese es el hilo cuya identificación se puede encontrar en System.MainThreadID. - David Heffernan

Voté negativamente porque su respuesta es demostrablemente incorrecta. Synchronize no ejecuta el código en el subproceso principal del proceso. Lo ejecuta en el hilo de inicialización del módulo. La actualización de mi respuesta demuestra esto. - David Heffernan

Synchronize() no publica en el subproceso que inicializó el módulo, publica en cualquier subproceso especificado actualmente por MainThreadID. Puede especificar el subproceso que inicializó el módulo de forma predeterminada, pero puede cambiarlo a cualquier ID de subproceso que desee. O incluso puede asignar su propio WakeMainThread devolución de llamada para hacer lo que quieras, no tiene que publicar nada. - Rémy Lebeau

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.