Patrones
Los patrones que se usan de verdad con FoxCore.Runtime 3.4.2: el lote, el lote grande, el progreso con cancelación, los handlers, la pizarra, los eventos y el entorno del worker. Y al final, cómo se rompe esto.
1. El lote paralelo: el caso más común
Lanzar N guiones, bombear hasta que no quede ninguno vivo, y recoger. El bucle lo escribe el host: no hay ninguna llamada que bloquee esperando, porque eso es justo la interfaz congelada que este producto evita.
* lcJwt es TU licencia, y GuionDe() es TU funcion: devuelve el texto
* del guion que va a correr cada tarea. Los dos son tuyos, no del
* producto.
LOCAL loRt, i, lnTotal, lnVivas
LOCAL ARRAY laIds[1]
loRt = CREATEOBJECT("FoxCore.Runtime")
loRt.Activate(lcJwt)
loRt.Attach(_VFP)
loRt.MaxWorkers = 8
loRt.DefaultTimeoutSeconds = 300
lnTotal = 30
DIMENSION laIds[lnTotal]
FOR i = 1 TO lnTotal
laIds[i] = loRt.RunScript(GuionDe(i), "lote " + TRANSFORM(i)) && el guion corre en OTRO proceso
ENDFOR
DO WHILE .T.
lnVivas = 0
FOR i = 1 TO lnTotal
IF !loRt.IsCompleted(laIds[i])
lnVivas = lnVivas + 1
ENDIF
ENDFOR
IF lnVivas = 0
EXIT
ENDIF
loRt.Pump()
DOEVENTS
ENDDO
FOR i = 1 TO lnTotal
? laIds[i], loRt.GetState(laIds[i]), loRt.GetResult(laIds[i]), loRt.GetError(laIds[i])
ENDFOR
loRt.Shutdown()2. El lote de cientos o miles
El bucle de arriba pregunta tarea por tarea. Con 400 tareas son 400 llamadas COM por vuelta sólo para saber quién sigue vivo, y a 0,08 ms cada una, unos 32 ms por vuelta gastados en preguntar. Con lotes de miles no escala. La forma buena es PollCompleted(), que entrega los ids terminados desde la última llamada en UNA sola llamada, y Forget() para liberar cada resultado según se lee.
LOCAL lcFeed, lnHechas, i, loStats
LOCAL ARRAY laTerm[1]
loRt.KeepCompletedTasks = 5000 && que la purga no se lleve lo que no has leído
lnHechas = 0
DO WHILE lnHechas < lnTotal
loRt.Pump()
lcFeed = loRt.PollCompleted() && UNA llamada, entregue lo que entregue
IF !EMPTY(lcFeed)
FOR i = 1 TO ALINES(laTerm, lcFeed, 1, ";")
* ... GetState / GetResult de laTerm[i] ...
loRt.Forget(laTerm[i]) && purga explícita, sin carreras
lnHechas = lnHechas + 1
ENDFOR
* GetStats() devuelve JSON como cadena: se parsea con la libreria
* que uses (aqui, jsonfox). el progreso honesto sale de los contadores de VIDA
loStats = loJson.Parse(loRt.GetStats())
THISFORM.lblProgreso.Caption = "Procesando " + ;
TRANSFORM(loStats.finishedEver) + " de " + TRANSFORM(lnTotal)
ENDIF
DOEVENTS
ENDDOY dos trampas más que este bucle evita: con KeepCompletedTasks en su defecto de 200 y un lote de 1.000 con recogida diferida, los primeros 800 resultados desaparecen antes de leerlos y GetState() empieza a contestar NotFound. Y el canal de terminados está acotado a 5.000 ids: si nadie lo drena se pierden los más viejos, y GetStats() lo dice en completedDropped, y no es pérdida de datos, la tarea sigue en el registro.
3. Progreso y cancelación, en un formulario
El sink no te llama desde un hilo de .NET: te llama Pump(), en TU hilo. Por eso un método del sink puede tocar la pantalla sin miedo. Y en un formulario, el sitio del Pump() es un Timer.
* oRt y cTaskId son PROPIEDADES que le anades tu al formulario, y
* Terminado() un metodo tuyo. El boceto es del lado del host.
* Init del formulario
THIS.oRt = CREATEOBJECT("FoxCore.Runtime")
THIS.oRt.Activate(lcJwt)
THIS.oRt.Attach(_VFP)
THIS.oRt.SetEventSink(THISFORM) && THISFORM tiene OnProgress y OnLog
THIS.cTaskId = ""
* Click del boton "Procesar"
THISFORM.cTaskId = THISFORM.oRt.RunScript(lcGuion, "cierre mensual")
THISFORM.timer1.Enabled = .T.
* Timer, Interval = 100
THISFORM.oRt.Pump()
IF !EMPTY(THISFORM.cTaskId) AND THISFORM.oRt.IsCompleted(THISFORM.cTaskId)
THISFORM.timer1.Enabled = .F.
THISFORM.Terminado()
ENDIF
* Click del boton "Cancelar"
THISFORM.oRt.Cancel(THISFORM.cTaskId)
* Destroy
THISFORM.oRt.ClearEventSink() && IMPORTANTE: mientras el runtime lo tenga, el formulario sigue vivo
THISFORM.oRt.Shutdown()Y en el guion, para que Cancel() sirva de algo:
SCAN
IF runtime.IsCancelled()
EXIT
ENDIF
* ... trabajo ...
IF MOD(RECNO(), 500) = 0
runtime.Progress(100 * RECNO() / RECCOUNT(), "procesando...")
ENDIF
ENDSCAN4. El worker te pide algo a ti: handlers
El caso clásico es un correlativo. Registras un objeto tuyo con un nombre, y el guion lo llama con runtime.Call().
loRt.RegisterHandler("SiguienteFolio", loCaja, "Siguiente")
DEFINE CLASS CajaDeFolios AS Custom
nUltimo = 0
FUNCTION Siguiente(tcSerie) && sin un solo bloqueo, y no hace falta
THIS.nUltimo = THIS.nUltimo + 1
RETURN TRANSFORM(tcSerie) + "-" + PADL(THIS.nUltimo, 4, "0")
ENDFUNC
ENDDEFINElcFolio = runtime.Call('SiguienteFolio', 'FRA')
* O sin TRY/CATCH, que no lanza nunca:
lcJson = runtime.TryCallJson('SiguienteFolio', 'FRA')
IF 'true' $ LOWER(lcJson)
* ... parsea el JSON como prefieras ...
ENDIFY aquí está la propiedad que importa: Pump() corre en tu hilo y atiende las peticiones de una en una. El objeto del host no lleva ningún bloqueo y aun así ocho workers pidiendo a la vez no producen ni un folio duplicado, verificado con 400 tareas y 410 peticiones. La serialización la da el Pump, no el objeto.
5. Pasarle datos al worker: la pizarra
Por orden de preferencia: dentro del propio guion (es texto, genéralo con TEXTMERGE); la pizarra; un fichero, para lotes de parámetros grandes; y Call() a un handler, que es el más caro y se reserva para lo que TIENE que resolverse en el host. Nunca por variables PUBLIC ni por cursores: no se heredan.
* En el host, antes de lanzar:
loRt.Publish("cRutaDatos", "\\srv\datos")
* En el guion:
lcRuta = runtime.GetValue("cRutaDatos") && sin coste de cola ni de pump6. Enterarse sin preguntar: eventos
SetEventSink() es adicional: GetProgress() y GetProgressMessage() siguen funcionando igual sin él. Lo que cambia es que te avisan en vez de tener que preguntar.
DEFINE CLASS EcoDeProgreso AS Custom
nAvisos = 0
cUltimo = ""
*-- OnProgress y OnLog son OBLIGATORIOS: sin los dos, SetEventSink da .F.
PROCEDURE OnProgress(tcTaskId, tnPercent, tcMessage)
THIS.nAvisos = THIS.nAvisos + 1
THIS.cUltimo = TRANSFORM(tnPercent) + "% " + TRANSFORM(tcMessage)
ENDPROC
PROCEDURE OnLog(tcTaskId, tcLevel, tcMessage)
ENDPROC
*-- Opcionales: se detectan si estan.
PROCEDURE OnTaskCompleted(tcTaskId, tcState)
ENDPROC
PROCEDURE OnTaskFailed(tcTaskId, tcError)
ENDPROC
ENDDEFINE7. El entorno del worker
Por defecto MaxTasksPerWorker vale 1: un proceso por tarea, aislamiento total. Subiéndolo, el worker se reutiliza: 91% menos procesos y 70% menos tiempo, medido sobre 120 tareas, y entonces hacen falta los dos guiones: el de inicio monta el entorno una vez, y el de reposición limpia entre tareas.
loRt.MaxTasksPerWorker = 20
loRt.WorkerInitScript = "SET PROCEDURE TO miapp.app ADDITIVE" + CHR(13) + CHR(10) + ;
"SET PATH TO " + lcRutaDatos && una vez por worker, antes de su primera tarea
loRt.WorkerResetScript = "CLOSE TABLES ALL" && entre tareas de un worker reutilizado
loRt.WorkerInitFailurePolicy = "abort"Cómo se rompe esto
Las reglas duras, con la consecuencia de saltárselas:
| Regla | Consecuencia |
|---|---|
| Llama a Pump() | Los Call() de los workers no se atienden y vencen su plazo. Síntoma: «las tareas no avanzan». |
| Llama a Shutdown() | Quedan procesos vfp9.exe huérfanos ocupando memoria. |
| Pump() sólo desde el hilo del Attach | Desde otro hilo lanza un error controlado. |
| Nada de DOEVENTS dentro de un handler | Reentra en Pump(), que devuelve 0 sin hacer nada. No lanza, pero no atiende. |
| ClearEventSink() en el Destroy | Mientras el runtime tenga el sink, el formulario sigue vivo: fuga de referencia. |
| Un handler VFP sólo falla hacia fuera con THROW | ERROR "..." y los errores de tiempo de ejecución no cruzan a COM: el método sigue en la línea siguiente y devuelve como si nada. |
| Y aun con THROW, el worker recibe «Procedure canceled.» | VFP no marshala el Message de la excepción al cruzar a COM. Si necesitas explicar por qué falló, guárdalo en una propiedad del objeto del host o devuélvelo como valor. |
| VFP ignora en silencio los argumentos que le sobran | Llamar a un handler de un parámetro con tres no da error: llega el primero. |
Antes de paralelizar: mira los datos
Esto es lo que más veces convierte un candidato aparente en un no-candidato:
- Los workers abren las tablas por su cuenta. Si el trabajo escribe en las mismas tablas desde ocho procesos, hay contención y bloqueos reales, los mismos que tendrías con ocho usuarios. Paraleliza lecturas y escrituras particionadas: cada worker su rango, su fichero de salida, su rama de datos.
- Una transacción no cruza procesos. No hay transacción distribuida: si el trabajo tiene que ser atómico de principio a fin, o cabe entero dentro de una tarea, o no es candidato.
- SET EXCLUSIVE, PACK, REINDEX y ZAP necesitan uso exclusivo. Sirven como tarea única en su ventana, nunca como lote paralelo sobre la misma tabla.
- Cuidado con los ficheros temporales de nombre fijo. Ocho workers escribiendo
c:\temp\salida.txtse pisan. UsaSYS(2015)o el TaskId en el nombre. - Los correlativos y contadores son justo el caso del handler del host, no de «que cada worker lea y sume».
RunAll ni WaitFor: no tienen equivalente, y el porqué está explicado en «Lo que ya no existe», al final de la Referencia de API. Siguiente: Referencia API ->