IR
irwinrodriguez.dev
Volver a documentación

Ejemplos

Tres programas completos, tal como se ejecutan. Están adaptados de los ejemplos que viven en el paquete de FoxCore.Runtime 3.4.2, en la carpeta ejemplos\: se pueden copiar, pegar en un .prg y ejecutar desde el IDE de Visual FoxPro.

Lo único que se ha cambiado respecto a los del paquete es de dónde sale la licencia. Los .prg del paquete leen el JWT de un fichero del disco porque son ficheros sueltos que ejecutas a mano y tienen que sacarlo de algún sitio; aquí va en una constante, que es lo que hace una aplicación de verdad. Ver Licencias.

1. Una tarea larga que avisa por dónde va

El worker llama a runtime.Progress(), y aquí se recoge de las dos formas: preguntando con GetProgress() y siendo avisado por un sink. La que importa entender es la segunda.

EcoDeProgreso: el avance de una tarea, preguntando y con un sink
LOCAL loRt, loEco, lcId, lcScript, lcJwt

lcJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...."   && pega aquí el JWT de tu portal de cliente

loRt = CREATEOBJECT("FoxCore.Runtime")
IF !loRt.Activate(lcJwt)
    MESSAGEBOX("La licencia de FoxCore no es valida.", 16, "FoxCore")
    RETURN
ENDIF
loRt.Attach(_VFP)

loEco = CREATEOBJECT("EcoDeProgreso")
? "sink aceptado :", loRt.SetEventSink(loEco)

TEXT TO lcScript NOSHOW
FOR i = 1 TO 5
    runtime.Progress(i * 20, 'paso ' + TRANSFORM(i) + ' de 5')
    =INKEY(0.2)
ENDFOR
runtime.Log('info', 'terminado sin novedad')
RETURN 'listo'
ENDTEXT

lcId = loRt.RunScript(lcScript, "con progreso")
DO WHILE !loRt.IsCompleted(lcId)
    loRt.Pump()                             && el sink se llama DESDE AQUÍ
ENDDO
loRt.Pump(200, 1000)                        && una última vuelta: pueden quedar avisos en cola

? "avisos recibidos :", loEco.nAvisos
? "ultimo aviso     :", loEco.cUltimo
? "ultimo log       :", loEco.cLog
? "y sin sink       :", loRt.GetProgress(lcId), loRt.GetProgressMessage(lcId)

loRt.ClearEventSink()                       && importante: mientras lo tenga, tu objeto vive
loRt.Shutdown()
RETURN


DEFINE CLASS EcoDeProgreso AS Custom
    nAvisos = 0
    cUltimo = ""
    cLog    = ""

    *-- 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)
        THIS.cLog = TRANSFORM(tcLevel) + ": " + TRANSFORM(tcMessage)
    ENDPROC

    *-- Este es opcional: se detecta si esta, y si no, no pasa nada.
    PROCEDURE OnTaskCompleted(tcTaskId, tcState)
    ENDPROC
ENDDEFINE
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 el Pump(200, 1000) del final no es adorno: cuando la tarea termina puede quedar algún aviso en la cola, y sin esa última vuelta no se entrega.

Qué verás

sink aceptado : .T.
avisos recibidos :          5
ultimo aviso     : 100% paso 5 de 5
ultimo log       : info: terminado sin novedad
y sin sink       : 100,00 paso 5 de 5

2. Repartir una tabla entre varios workers

La receta más pedida, y la que más se hace mal. La clave es que NO se reparte el cursor: se reparte el RANGO. Cada worker abre la tabla por su cuenta y se ocupa de un tramo, así que no hay nada compartido y no hace falta bloquear.

Sumar una tabla por tramos en cuatro workers
LOCAL loRt, lcDbf, i, lnDesde, lnHasta, lnPorTramo, lnTotal, lnHechas, lcTerm, lcJwt
LOCAL ARRAY laIds[4], laTerm[1]

lcJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...."   && pega aquí el JWT de tu portal de cliente

*-- Una tabla de ejemplo con 400 lineas
CREATE CURSOR ventas (id I, importe Y)
FOR i = 1 TO 400
    INSERT INTO ventas VALUES (i, i * 1.25)
ENDFOR
lcDbf = ADDBS(SYS(2023)) + "r09_ventas.dbf"
COPY TO (lcDbf)
USE IN SELECT("ventas")

loRt = CREATEOBJECT("FoxCore.Runtime")
IF !loRt.Activate(lcJwt)
    MESSAGEBOX("La licencia de FoxCore no es valida.", 16, "FoxCore")
    RETURN
ENDIF
loRt.Attach(_VFP)
loRt.MaxWorkers = 4

*-- Cuatro tramos de 100. cada worker abre la tabla POR SU CUENTA y suma el suyo.
lnPorTramo = 100
FOR i = 1 TO 4
    lnDesde = (i - 1) * lnPorTramo + 1
    lnHasta = i * lnPorTramo
    laIds[i] = loRt.RunScript( ;
        "USE '" + lcDbf + "' IN 0 SHARED ALIAS ventas" + CHR(13) + CHR(10) + ;
        "SELECT ventas" + CHR(13) + CHR(10) + ;
        "SUM importe TO lnT FOR BETWEEN(id, " + TRANSFORM(lnDesde) + ", " + TRANSFORM(lnHasta) + ")" + CHR(13) + CHR(10) + ;
        "USE IN ventas" + CHR(13) + CHR(10) + ;
        "RETURN lnT", "tramo " + TRANSFORM(i))
ENDFOR

lnHechas = 0
DO WHILE lnHechas < 4
    loRt.Pump()
    lcTerm = loRt.PollCompleted()
    IF !EMPTY(lcTerm)
        lnHechas = lnHechas + ALINES(laTerm, lcTerm, 1, ";")
    ENDIF
ENDDO

*-- y se junta aquí, donde hay un solo hilo
lnTotal = 0
FOR i = 1 TO 4
    ? "  tramo " + TRANSFORM(i) + " :", loRt.GetState(laIds[i]), loRt.GetResult(laIds[i])
    IF loRt.GetState(laIds[i]) == "Completed"
        lnTotal = lnTotal + loRt.GetResult(laIds[i])
    ENDIF
ENDFOR
? "total en paralelo :", lnTotal, "  (esperado 100250,00)"

ERASE (lcDbf)
loRt.Shutdown()
RETURN
El bucle usa PollCompleted(), que devuelve en UNA llamada los ids terminados desde la última vez. Con cuatro tramos da igual, pero con cientos es la diferencia entre una llamada COM por vuelta y N. Y la suma se hace en el host, donde hay un solo hilo y ningún problema.

Qué verás

  tramo 1 : Completed       6312,50
  tramo 2 : Completed      18812,50
  tramo 3 : Completed      31312,50
  tramo 4 : Completed      43812,50
total en paralelo :     100250,00   (esperado 100250,00)

3. Un handler propio, llamado desde el worker

El caso clásico: un correlativo. Ocho procesos pidiendo número a la vez y ni uno repetido, con un objeto que no tiene un solo bloqueo.

CajaDeFolios: un correlativo servido por el host
LOCAL loRt, loCaja, i, lnHechas, lcTerminadas, lcJwt
LOCAL ARRAY laIds[8], laTerm[1]

lcJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...."   && pega aquí el JWT de tu portal de cliente

loRt = CREATEOBJECT("FoxCore.Runtime")
IF !loRt.Activate(lcJwt)
    MESSAGEBOX("La licencia de FoxCore no es valida.", 16, "FoxCore")
    RETURN
ENDIF
loRt.Attach(_VFP)
loRt.MaxWorkers = 4

loCaja = CREATEOBJECT("CajaDeFolios")
? "handler aceptado :", loRt.RegisterHandler("SiguienteFolio", loCaja, "Siguiente")

FOR i = 1 TO 8
    laIds[i] = loRt.RunScript("RETURN runtime.Call('SiguienteFolio', 'FRA')", "folio " + TRANSFORM(i))
ENDFOR

lnHechas = 0
DO WHILE lnHechas < 8
    loRt.Pump()                             && aquí se atienden las llamadas
    lcTerminadas = loRt.PollCompleted()
    IF !EMPTY(lcTerminadas)
        lnHechas = lnHechas + ALINES(laTerm, lcTerminadas, 1, ";")
    ENDIF
ENDDO

? "folios emitidos :", loCaja.nUltimo
FOR i = 1 TO 8
    ?? " " + TRANSFORM(loRt.GetResult(laIds[i]))
ENDFOR

loRt.Shutdown()
RETURN


DEFINE CLASS CajaDeFolios AS Custom
    nUltimo = 0

    *-- sin un solo bloqueo, y no hace falta: esto se ejecuta en el hilo del host,
    *-- de una llamada en una llamada.
    FUNCTION Siguiente(tcSerie)
        THIS.nUltimo = THIS.nUltimo + 1
    RETURN TRANSFORM(tcSerie) + "-" + PADL(THIS.nUltimo, 4, "0")
    ENDFUNC
ENDDEFINE
Porque el host los atiende de uno en uno, dentro de Pump(), en tu hilo. La serialización la da el Pump, no el objeto. runtime.Call() es síncrona: bloquea al worker hasta que tu Pump() la atienda, y si no bombeas, vence su plazo y el guion recibe un error legible.

Qué verás

handler aceptado : .T.
folios emitidos :          8 FRA-0001 FRA-0002 FRA-0003 FRA-0004 FRA-0005 FRA-0006 FRA-0007 FRA-0008
¿Vienes del motor clásico? Estos ejemplos no usan FoxCoreClass, Run() ni WaitFor(). La tabla de equivalencias está en «Lo que ya no existe», al final de la Referencia de API.

Siguiente: Referencia API ->