IR
irwinrodriguez.dev
Zurück zur Dokumentation

Muster

Die Muster, die mit FoxCore.Runtime 3.4.2 wirklich benutzt werden: der Stapel, der große Stapel, Fortschritt mit Abbruch, Handler, die Tafel, Ereignisse und die Worker-Umgebung. Und am Ende, wie das kaputtgeht.

1. Der parallele Stapel - der häufigste Fall

N Skripte starten, pumpen, bis keines mehr lebt, und einsammeln. Die Schleife schreibt der Host: es gibt keinen Aufruf, der wartend blockiert, denn genau das ist die eingefrorene Oberfläche, die dieses Produkt vermeidet.

Ein Stapel aus 30 Skripten: starten, pumpen und einsammeln
* 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))   && das Skript läuft in einem ANDEREN Prozess
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. Der Stapel aus Hunderten oder Tausenden

Die Schleife oben fragt Aufgabe für Aufgabe. Bei 400 Aufgaben sind das 400 COM-Aufrufe pro Runde, nur um zu wissen, wer noch lebt - bei 0,08 ms je Aufruf rund 32 ms pro Runde nur zum Fragen. Bei Tausenden skaliert das nicht. Der richtige Weg ist PollCompleted(), das die seit dem letzten Aufruf beendeten IDs in EINEM Aufruf liefert, und Forget(), um jedes Ergebnis beim Lesen freizugeben.

Großer Stapel mit PollCompleted() und Forget()
LOCAL lcFeed, lnHechas, i, loStats
LOCAL ARRAY laTerm[1]

loRt.KeepCompletedTasks = 5000          && damit die Bereinigung nicht wegnimmt, was Sie nicht gelesen haben
lnHechas = 0

DO WHILE lnHechas < lnTotal
    loRt.Pump()
    lcFeed = loRt.PollCompleted()       && EIN Aufruf, was immer er liefert
    IF !EMPTY(lcFeed)
        FOR i = 1 TO ALINES(laTerm, lcFeed, 1, ";")
            * ... GetState / GetResult de laTerm[i] ...
            loRt.Forget(laTerm[i])      && explizite Bereinigung, ohne Wettlauf
            lnHechas = lnHechas + 1
        ENDFOR
        * GetStats() devuelve JSON como cadena: se parsea con la libreria
        * que uses (aqui, jsonfox). ehrlicher Fortschritt kommt aus den LEBENSZEIT-Zahlern
        loStats = loJson.Parse(loRt.GetStats())
        THISFORM.lblProgreso.Caption = "Procesando " + ;
            TRANSFORM(loStats.finishedEver) + " de " + TRANSFORM(lnTotal)
    ENDIF
    DOEVENTS
ENDDO
finishedEver, nicht completed. Es sind zwei verschiedene Familien, und sie sehen sich ahnlich: die *Ever zahlen, was seit dem Start der Runtime PASSIERT ist, und gehen nie zurück; total, completed und Konsorten sind die MOMENTAUFNAHME der Registry, und Forget() und die Bereinigung ziehen ab. Mit dieser Schleife - die in jeder Runde Forget() aufruft - bleibt die Momentaufnahme bei null, und das ist richtig. Der Fortschritt muss aus der Historie kommen.

Und zwei weitere Fallen, die diese Schleife vermeidet: mit KeepCompletedTasks im Standardwert 200 und einem Stapel von 1.000 mit später Abholung verschwinden die ersten 800 Ergebnisse, bevor Sie sie lesen, und GetState() antwortet NotFound. Und der Kanal der Beendeten ist auf 5.000 IDs begrenzt: leert ihn niemand, gehen die altesten verloren, und GetStats() sagt es in completedDropped - kein Datenverlust, die Aufgabe bleibt in der Registry.

3. Fortschritt und Abbruch, in einem Formular

Der Sink wird nicht von einem .NET-Thread aufgerufen: ihn ruft Pump() auf, in IHREM Thread. Deshalb darf eine Sink-Methode den Bildschirm anfassen. Und in einem Formular gehört Pump() in einen Timer.

Formular mit Fortschritt und Abbrechen-Schaltfläche
* 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 hat OnProgress und 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()          && WICHTIG: solange die Runtime es hält, bleibt das Formular am Leben
THISFORM.oRt.Shutdown()

Und im Skript, damit Cancel() etwas bewirkt:

Skript, das Cancel() beachtet und den Fortschritt meldet
SCAN
    IF runtime.IsCancelled()
        EXIT
    ENDIF
    * ... trabajo ...
    IF MOD(RECNO(), 500) = 0
        runtime.Progress(100 * RECNO() / RECCOUNT(), "procesando...")
    ENDIF
ENDSCAN
Ein Skript, das IsCancelled() nie prüft, hält nur mit Kill() an - das den Prozess per PID beendet - oder wenn seine Frist abläuft. Das ist der Unterschied zwischen kooperativem und hartem Abbruch, und er muss beim Entwurf der Aufgabe entschieden werden, nicht danach.

4. Der Worker fragt Sie etwas - Handler

Der klassische Fall ist eine laufende Nummer. Sie registrieren ein eigenes Objekt unter einem Namen, und das Skript ruft es mit runtime.Call() auf.

CajaDeFolios: der im Host registrierte Handler
loRt.RegisterHandler("SiguienteFolio", loCaja, "Siguiente")

DEFINE CLASS CajaDeFolios AS Custom
    nUltimo = 0
    FUNCTION Siguiente(tcSerie)          && ohne einen einzigen Lock, und es braucht keinen
        THIS.nUltimo = THIS.nUltimo + 1
    RETURN TRANSFORM(tcSerie) + "-" + PADL(THIS.nUltimo, 4, "0")
    ENDFUNC
ENDDEFINE
Das Skript holt eine Nummer mit runtime.Call()
lcFolio = 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 ...
ENDIF

Und hier ist die Eigenschaft, auf die es ankommt: Pump() läuft in Ihrem Thread und bedient die Anfragen eine nach der anderen. Das Host-Objekt trägt keinen einzigen Lock, und trotzdem erzeugen acht gleichzeitig fragende Worker keine einzige Dublette - verifiziert mit 400 Aufgaben und 410 Anfragen. Die Serialisierung kommt vom Pump, nicht vom Objekt.

Es gibt drei eingebaute Handler, die immer verfügbar sind, um den Kanal ohne eine Zeile Code zu testen: echo, upper und add. Und RegisterHandler() liefert .F., ohne zu werfen, wenn das Objekt diese Methode nicht bietet - der Fehler taucht also im Init des Formulars auf und nicht sechs Stunden später im Worker.

5. Daten zum Worker bringen - die Tafel

Nach Vorzug: im Skript selbst (es ist Text, bauen Sie es mit TEXTMERGE); die Tafel; eine Datei, für große Parametermengen; und Call() an einen Handler, das Teuerste, reserviert für das, was im Host geloest werden MUSS. Nie über PUBLIC-Variablen oder Cursor: die werden nicht vererbt.

Einen Wert auf der Tafel veröffentlichen und im Skript lesen
* En el host, antes de lanzar:
loRt.Publish("cRutaDatos", "\\srv\datos")

* En el guion:
lcRuta = runtime.GetValue("cRutaDatos")      && ohne Warteschlangen- und Pump-Kosten

6. Benachrichtigt werden statt zu fragen - Ereignisse

SetEventSink() ist zusätzlich: GetProgress() und GetProgressMessage() funktionieren auch ohne. Was sich ändert: Sie werden benachrichtigt, statt fragen zu müssen.

EcoDeProgreso: ein Ereignis-Sink
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
ENDDEFINE

7. Die Worker-Umgebung

Standardmäßig ist MaxTasksPerWorker 1: ein Prozess pro Aufgabe, volle Isolation. Erhöht man ihn, wird der Worker wiederverwendet - 91% weniger Prozesse und 70% weniger Zeit, gemessen über 120 Aufgaben - und dann braucht es die beiden Skripte: das Startskript baut die Umgebung einmal auf, das Reset-Skript raumt zwischen den Aufgaben auf.

Worker wiederverwenden mit Start- und Reset-Skript
loRt.MaxTasksPerWorker = 20
loRt.WorkerInitScript  = "SET PROCEDURE TO miapp.app ADDITIVE" + CHR(13) + CHR(10) + ;
                         "SET PATH TO " + lcRutaDatos      && einmal pro Worker, vor seiner ersten Aufgabe
loRt.WorkerResetScript = "CLOSE TABLES ALL"                && zwischen Aufgaben eines wiederverwendeten Workers
loRt.WorkerInitFailurePolicy = "abort"
Zwischen den Aufgaben wird nichts Generisches aufgeraumt, und das ist keine Faulheit: jede Bereinigung, die breit genug wäre, die vorige Aufgabe zu löschen, wurde auch die Umgebung zerstoren, die WorkerInitScript gerade aufgebaut hat. Dafür gibt es WorkerResetScript. Und ein Worker kehrt nur in den Pool zurück, wenn seine Aufgabe als Completed endete: alles andere verbrennt ihn, denn ein Skript, das auf halbem Weg scheiterte, kann einen Cursor, eine Transaktion oder einen Lock hinterlassen haben.

Wie das kaputtgeht

Die harten Regeln, mit der Folge, sie zu ignorieren:

RegelFolge
Pump() aufrufenDie Call()s der Worker werden nicht bedient und laufen in ihre Frist. Symptom: "die Aufgaben kommen nicht voran".
Shutdown() aufrufenVerwaiste vfp9.exe-Prozesse bleiben ubrig und belegen Speicher.
Pump() nur aus dem Attach-ThreadAus einem anderen Thread gibt es einen kontrollierten Fehler.
Kein DOEVENTS in einem HandlerEs tritt erneut in Pump() ein, das 0 liefert und nichts tut. Es wirft nicht, bedient aber niemanden.
ClearEventSink() im DestroySolange die Runtime den Sink hält, bleibt das Formular am Leben: ein Referenzleck.
Ein VFP-Handler scheitert nach außen nur mit THROWERROR "..." und Laufzeitfehler queren COM nicht: die Methode macht in der nachsten Zeile weiter und kehrt zurück, als wäre nichts.
Und selbst mit THROW erhält der Worker "Procedure canceled."VFP marshallt die Message der Ausnahme nicht über COM. Wenn Sie erklären müssen, warum es scheiterte, legen Sie es in eine Eigenschaft des Host-Objekts oder geben es als Wert zurück.
VFP ignoriert uberzahlige Argumente stillschweigendEinen Handler mit einem Parameter mit dreien aufzurufen, gibt keinen Fehler: der erste kommt an.

Vor dem Parallelisieren: sehen Sie sich die Daten an

Das macht am häufigsten aus einem scheinbaren Kandidaten einen Nicht-Kandidaten:

  • Die Worker öffnen die Tabellen selbst. Schreibt die Arbeit aus acht Prozessen in dieselben Tabellen, gibt es echte Konkurrenz und echte Sperren - dieselben wie bei acht Benutzern. Parallelisieren Sie Lesevorgange und partitionierte Schreibvorgange: jeder Worker sein Bereich, seine Ausgabedatei, sein Datenzweig.
  • Eine Transaktion quert keine Prozesse. Es gibt keine verteilte Transaktion: muss die Arbeit von Anfang bis Ende atomar sein, passt sie entweder ganz in eine Aufgabe, oder sie ist kein Kandidat.
  • SET EXCLUSIVE, PACK, REINDEX und ZAP brauchen exklusiven Zugriff. Sie taugen als einzige Aufgabe in ihrem Fenster, nie als paralleler Stapel über dieselbe Tabelle.
  • Vorsicht mit temporaren Dateien fester Namen. Acht Worker, die c:\temp\out.txt schreiben, treten sich auf die Fusse. Nehmen Sie SYS(2015) oder die TaskId in den Namen.
  • Laufende Nummern und Zahler sind genau der Fall für den Host-Handler, nicht für "jeder Worker liest und addiert".
Kommen Sie von der klassischen Engine? Hier finden Sie weder RunAll noch WaitFor: sie haben kein Aquivalent, und der Grund steht in "Was es nicht mehr gibt", am Ende der API-Referenz.

Weiter: API-Referenz ->