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.
* 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.
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
ENDDOUnd 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.
* 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:
SCAN
IF runtime.IsCancelled()
EXIT
ENDIF
* ... trabajo ...
IF MOD(RECNO(), 500) = 0
runtime.Progress(100 * RECNO() / RECCOUNT(), "procesando...")
ENDIF
ENDSCAN4. 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.
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
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 ...
ENDIFUnd 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.
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.
* En el host, antes de lanzar:
loRt.Publish("cRutaDatos", "\\srv\datos")
* En el guion:
lcRuta = runtime.GetValue("cRutaDatos") && ohne Warteschlangen- und Pump-Kosten6. 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.
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. 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.
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"Wie das kaputtgeht
Die harten Regeln, mit der Folge, sie zu ignorieren:
| Regel | Folge |
|---|---|
| Pump() aufrufen | Die Call()s der Worker werden nicht bedient und laufen in ihre Frist. Symptom: "die Aufgaben kommen nicht voran". |
| Shutdown() aufrufen | Verwaiste vfp9.exe-Prozesse bleiben ubrig und belegen Speicher. |
| Pump() nur aus dem Attach-Thread | Aus einem anderen Thread gibt es einen kontrollierten Fehler. |
| Kein DOEVENTS in einem Handler | Es tritt erneut in Pump() ein, das 0 liefert und nichts tut. Es wirft nicht, bedient aber niemanden. |
| ClearEventSink() im Destroy | Solange die Runtime den Sink hält, bleibt das Formular am Leben: ein Referenzleck. |
| Ein VFP-Handler scheitert nach außen nur mit THROW | ERROR "..." 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 stillschweigend | Einen 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.txtschreiben, treten sich auf die Fusse. Nehmen SieSYS(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".
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 ->