Este es mi próximo proyecto de prueba para ver qué biblioteca de subprocesos para Delphi me conviene mejor para mi tarea de "escaneo de archivos" que me gustaría procesar en varios subprocesos / en un grupo de subprocesos.
Para repetir mi objetivo: transformar mi "escaneo de archivos" secuencial de más de 500-2000 archivos del enfoque sin hilos a uno roscado. No debería tener 500 subprocesos ejecutándose al mismo tiempo, por lo tanto, me gustaría usar un grupo de subprocesos. Un grupo de subprocesos es una clase similar a una cola que alimenta varios subprocesos en ejecución con la siguiente tarea de la cola.
El primer intento (muy básico) se realizó simplemente extendiendo la clase TThread e implementando el método Execute (mi analizador de cadenas roscado).
Como Delphi no tiene una clase de grupo de subprocesos implementada de fábrica, en mi segundo intento he intentado usar OmniThreadLibrary de Primoz Gabrijelcic.
OTL es fantástico, tiene muchísimas formas de ejecutar una tarea en segundo plano, un camino a seguir si desea tener un enfoque de "disparar y olvidar" para entregar la ejecución roscada de partes de su código.
Nota: lo que sigue sería más fácil de seguir si primero descarga el código fuente.
Mientras exploraba más formas de ejecutar algunas de mis funciones de manera enhebrada, decidí probar también la unidad "AsyncCalls.pas" desarrollada por Andreas Hausladen. Las llamadas asíncronas de Andy: la unidad de llamadas de función asincrónica es otra biblioteca que un desarrollador de Delphi puede usar para aliviar el dolor de implementar un enfoque roscado para ejecutar algún código.
Del blog de Andy: Con AsyncCalls puede ejecutar múltiples funciones al mismo tiempo y sincronizarlas en cada punto de la función o método que las inició ... La unidad AsyncCalls ofrece una variedad de prototipos de funciones para llamar a funciones asincrónicas ... Implementa un grupo de subprocesos! La instalación es súper fácil: solo use llamadas asíncronas desde cualquiera de sus unidades y tendrá acceso instantáneo a cosas como "ejecutar en un hilo separado, sincronizar la interfaz de usuario principal, esperar hasta que termine".
Además de las llamadas AsyncCalls de uso gratuito (licencia MPL), Andy también publica con frecuencia sus propias soluciones para el IDE de Delphi como "Delphi Speed Up" y "DDevExtensions". Estoy seguro de que ha oído hablar (si no lo está usando).
En esencia, todas las funciones AsyncCall devuelven una interfaz IAsyncCall que permite sincronizar las funciones. IAsnycCall expone los siguientes métodos:
//v 2.98 de asynccalls.pas
IAsyncCall = interfaz
// espera hasta que finalice la función y devuelve el valor de retorno
sincronización de funciones: entero;
// devuelve True cuando finaliza la función asincrónica
función terminada: booleana;
// devuelve el valor de retorno de la función asincrónica, cuando Finished es TRUE
función ReturnValue: Integer;
// le dice a AsyncCalls que la función asignada no debe ejecutarse en la amenaza actual
procedimiento ForceDifferentThread;
final;
Aquí hay una llamada de ejemplo a un método que espera dos parámetros enteros (devolver una IAsyncCall):
TAsyncCalls.Invoke (AsyncMethod, i, Random (500));
función TAsyncCallsForm.AsyncMethod (taskNr, sleepTime: integer): integer;
empezar
resultado: = sleepTime;
Sueño (sleepTime);
TAsyncCalls.VCLInvoke (
procedimiento
empezar
Log (Formato ('hecho> nr:% d / tareas:% d / dormido:% d', [tasknr, asyncHelper.TaskCount, sleepTime]));
final);
final;
TAsyncCalls.VCLInvoke es una forma de sincronizar con su hilo principal (hilo principal de la aplicación: la interfaz de usuario de su aplicación). VCLInvoke regresa de inmediato. El método anónimo se ejecutará en el hilo principal. También hay VCLSync que regresa cuando se llamó al método anónimo en el hilo principal.
Volviendo a mi tarea de "escaneo de archivos": al alimentar (en un bucle for) el grupo de subprocesos asynccalls con una serie de llamadas TAsyncCalls.Invoke (), las tareas se agregarán al grupo interno y se ejecutarán "cuando llegue el momento" ( cuando las llamadas agregadas previamente hayan terminado).
La función AsyncMultiSync definida en asnyccalls espera a que finalicen las llamadas asíncronas (y otros identificadores). Hay algunas formas sobrecargadas de llamar a AsyncMultiSync, y esta es la más simple:
función AsyncMultiSync (const Lista: gama de IAsyncCall; WaitAll: Boolean = True; Milisegundos: Cardenal = INFINITO): Cardenal;
Si quiero tener "esperar todo" implementado, necesito completar una matriz de IAsyncCall y hacer AsyncMultiSync en segmentos de 61.
Aquí hay una parte de TAsyncCallsHelper:
ADVERTENCIA: código parcial! (código completo disponible para descargar)
usos AsyncCalls;
tipo
TIAsyncCallArray = gama de IAsyncCall;
TIAsyncCallArrays = gama de TIAsyncCallArray;
TAsyncCallsHelper = clase
privado
fTasks: TIAsyncCallArrays;
propiedad Tareas: TIAsyncCallArrays leer fTareas
público
procedimiento Agregar tarea(const llamada: IAsyncCall);
procedimiento Esperar todos;
final;
ADVERTENCIA: código parcial!
procedimiento TAsyncCallsHelper.WaitAll;
var
i: entero;
empezar
para i: = alta (tareas) Abajo a Bajo (tareas) hacer
empezar
AsyncCalls.AsyncMultiSync (Tareas [i]);
final;
final;
De esta manera, puedo "esperar todo" en fragmentos de 61 (MAXIMUM_ASYNC_WAIT_OBJECTS), es decir, esperar matrices de IAsyncCall.
Con lo anterior, mi código principal para alimentar el grupo de subprocesos se ve así:
procedimiento TAsyncCallsForm.btnAddTasksClick (Remitente: TObject);
const
nrItems = 200;
var
i: entero;
empezar
asyncHelper.MaxThreads: = 2 * System.CPUCount;
ClearLog ('inicio');
para i: = 1 a nrItems hacer
empezar
asyncHelper.AddTask (TAsyncCalls.Invoke (AsyncMethod, i, Random (500)));
final;
Log ('todo en');
// espera todo
//asyncHelper.WaitAll;
// o permitir cancelar todo no iniciado haciendo clic en el botón "Cancelar todo":
mientras no asyncHelper.AllFinished hacer Application.ProcessMessages;
Registro ('terminado');
final;
También me gustaría tener una forma de "cancelar" aquellas tareas que están en el grupo pero que están esperando su ejecución.
Desafortunadamente, AsyncCalls.pas no proporciona una forma simple de cancelar una tarea una vez que se ha agregado al grupo de subprocesos. No hay IAsyncCall.Cancel o IAsyncCall.DontDoIfNotAlreadyExecuting o IAsyncCall.NeverMindMe.
Para que esto funcione, tuve que cambiar AsyncCalls.pas tratando de alterarlo lo menos posible, de modo que cuando Andy lanza una nueva versión solo tengo que agregar algunas líneas para que mi idea de "Cancelar tarea" funcione.
Esto es lo que hice: agregué un "procedimiento Cancelar" a IAsyncCall. El procedimiento Cancelar establece el campo "FCancelled" (agregado) que se verifica cuando el grupo está a punto de comenzar a ejecutar la tarea. Necesitaba modificar ligeramente IAsyncCall.Finished (para que los informes de una llamada finalizaran incluso cuando se cancelaran) y el procedimiento TAsyncCall.InternExecuteAsyncCall (no ejecutar la llamada si se ha cancelado).
Puede usar WinMerge para localizar fácilmente las diferencias entre el asynccall.pas original de Andy y mi versión alterada (incluida en la descarga).
Puede descargar el código fuente completo y explorar.
los Cancelar invocación El método evita que se invoque AsyncCall. Si AsyncCall ya está procesada, una llamada a CancelInvocation no tiene efecto y la función Cancelada devolverá False ya que AsyncCall no se canceló.
los Cancelado el método devuelve True si la cancelación de invocación de AsyncCall fue cancelada.
los Olvidar El método desvincula la interfaz IAsyncCall de la AsyncCall interna. Esto significa que si la última referencia a la interfaz IAsyncCall desaparece, la llamada asincrónica se seguirá ejecutando. Los métodos de la interfaz arrojarán una excepción si se llama después de llamar a Forget. La función asincrónica no debe llamar al subproceso principal porque podría ejecutarse después de que el mecanismo TThread.Synchronize / Queue cerró el RTL, lo que puede causar un bloqueo muerto.
Sin embargo, tenga en cuenta que todavía puede beneficiarse de mi AsyncCallsHelper si necesita esperar a que todas las llamadas asíncronas terminen con "asyncHelper.WaitAll"; o si necesita "Cancelar todo".