startnew run not net for ejemplo does asp multithreading delphi

multithreading - run - task startnew c#



Cierre en TTask.Run(AnonProc) no lanzado después de que AnonProc haya terminado (2)

Los métodos anónimos en Delphi crean un cierre, que mantiene las variables locales "circundantes" en contexto, hasta que el método anónimo haya finalizado. Si usa variables de interfaz, disminuirán su instancia referenciada no antes de que el método anónimo haya finalizado. Hasta aquí todo bien.

Al usar TTask.Run (AProc: TProc) con un método anónimo, esperaría que el cierre se libere cuando el hilo de trabajo asociado haya terminado de ejecutar "AProc". Esto no parece suceder sin embargo. Al finalizar el programa, cuando se libera el grupo de subprocesos (al que pertenece este subproceso generado por TTask), finalmente puede ver que se liberan estas instancias referenciadas localmente, es decir, se libera el cierre aparentemente.

La pregunta es si esta es una característica o un error. ¿O superviso algo aquí?

A continuación, después de TTask.Run (...) esperar esperaría que se llamara al destructor de LFoo, lo que no ocurre.

procedure Test3; var LFoo: IFoo; begin LFoo := TFoo.Create; TTask.Run( procedure begin Something(LFoo); end).Wait; // Wait for task to finish //After TTask.Run has finished, it should let go LFoo out of scope - which it does not apprently. end;

Lo que sigue es un caso de prueba completo, que muestra que un método anónimo "simple" funciona como se espera (Test2), pero cuando se lo ingresa en TTask.Run no funciona (Test3)

program InterfaceBug; {$APPTYPE CONSOLE} {$R *.res} uses System.Classes, System.SysUtils, System.Threading; type //Simple Interface/Class IFoo = interface(IInterface) [''{7B78D718-4BA1-44F2-86CB-DDD05EF2FC56}''] procedure Bar; end; TFoo = class(TInterfacedObject, IFoo) public constructor Create; destructor Destroy; override; procedure Bar; end; procedure TFoo.Bar; begin Writeln(''Foo.Bar''); end; constructor TFoo.Create; begin inherited; Writeln(''Foo.Create''); end; destructor TFoo.Destroy; begin Writeln(''Foo.Destroy''); inherited; end; procedure Something(const AFoo: IFoo); begin Writeln(''Something''); AFoo.Bar; end; procedure Test1; var LFoo: IFoo; begin Writeln(''Test1...''); LFoo := TFoo.Create; Something(LFoo); Writeln(''Test1 done.''); //LFoo goes out od scope, and the destructor gets called end; procedure Test2; var LFoo: IFoo; LProc: TProc; begin Writeln(''Test2...''); LFoo := TFoo.Create; LProc := procedure begin Something(LFoo); end; LProc(); Writeln(''Test2 done.''); //LFoo goes out od scope, and the destructor gets called end; procedure Test3; var LFoo: IFoo; begin Writeln(''Test3...''); LFoo := TFoo.Create; TTask.Run( procedure begin Something(LFoo); end).Wait; // Wait for task to finish //LFoo := nil; This would call TFoo''s destructor, //but it should get called automatically with LFoo going out of scope - which apparently does not happen! Writeln(''Test3 done.''); end; begin try Test1; //works Writeln; Test2; //works Writeln; Test3; //fails Writeln(''--------''); Writeln(''Expected: Three calls of Foo.Create and three corresponding ones of Foo.Destroy''); Writeln; Writeln(''Actual: The the third Foo.Destroy is missing and is executed when the program terminates, i.e. when the default ThreadPool gets destroyed.''); ReadLn; except on E: Exception do Writeln(E.ClassName, '': '', E.Message); end; end.


Este es un problema conocido: el subproceso de trabajo TThreadPool contiene una referencia a la última tarea ejecutada

Una variable temporal en TThreadPool.TQueueWorkerThread.Execute mantiene una referencia al último elemento de trabajo ejecutado (tarea), que solo se libera cuando finaliza el método Execute.

Al estar en un grupo, el subproceso generalmente se mantiene activo hasta que se destruye el grupo, lo que para el grupo Predeterminado significa durante la finalización de la unidad. Por lo tanto, las últimas tareas ejecutadas no se liberan hasta que finaliza el programa.


Hice un análisis más de este error para descubrir la verdadera razón por la cual el ITask se estaba TThreadPool.TQueueWorkerThread.Execute en TThreadPool.TQueueWorkerThread.Execute como se menciona en el problema conocido .

La siguiente línea de código que parece inocente es el problema:

Item := ThreadPool.FQueue.Dequeue;

¿Por qué es ese el caso? Debido a que TQueue<T>.Dequeue está marcado como en línea y ahora debe saber que el compilador no aplica la denominada optimización del valor devuelto para las funciones en línea que devuelven un tipo gestionado.

Esto significa que la línea antes realmente se traduce (simplifiqué mucho esto) en este código por el compilador. tmp es una variable generada por el compilador - reserva espacio en la pila en el prólogo del método:

tmp := ThreadPool.FQueue.Dequeue; Item := tmp;

Esta variable se finaliza al end del método. Puedes poner un punto de quiebre allí y uno en TTask.Destroy y luego verás que cuando la aplicación finaliza una vez que llega al final del método, esto desencadena la última instancia de TTask que se destruye porque se borra la variable de temperatura que lo mantiene con vida.

Usé un poco de hack para solucionar este problema localmente. TThreadPool.TQueueWorkerThread.Execute este procedimiento local para eliminar la variable temporal que se escabulle en el método TThreadPool.TQueueWorkerThread.Execute :

procedure InternalDequeue(var Item: IThreadPoolWorkItem); begin Item := ThreadPool.FQueue.Dequeue; end;

y luego cambió el código dentro del método:

InternalDequeue(Item);

Esto aún causará que Dequeue produzca una variable temporal, pero ahora solo vive dentro del método InternalDequeue y se borrará una vez que finalice.

Editar (11.11.2017): Esto se ha corregido en 10.2 en el compilador. Ahora inserta un bloque finally después de la asignación de la variable temp al real, por lo que la variable temp no causa una referencia adicional más de lo que debería.