Das kennen wir alle. Der Anwender klickt auf «Monatsabschluss», auf «Rechnungen erstellen» oder auf «PDF erzeugen», und die Anwendung bleibt stehen. Das Fenster wird weiß, sobald ein anderes darüberfährt, die Maus tut nichts, und Windows schreibt «reagiert nicht» in die Titelleiste. Die Verarbeitung läuft einwandfrei, von außen ist das aber nicht zu erkennen, und am Ende ruft der Anwender an und fragt, ob er den Rechner ausschalten soll.
Das passiert, weil Visual FoxPro eine Sache nach der anderen erledigt. Während es Ihre 180.000 Lieferscheinpositionen durchläuft, kann es den Bildschirm nicht neu zeichnen und die Maus nicht bedienen. Wir umgehen das seit Jahren mit DOEVENTS in der Schleife, mit einer zweiten .exe, die mit RUN /N gestartet wird und am Ende eine Kennzeichnungsdatei hinterlässt, oder indem die Arbeit per geplanter Aufgabe in die Nacht verschoben wird.
FoxCore ist die Bibliothek, die ich geschrieben habe, um das anders zu lösen: Sie geben die Arbeit an andere Prozesse, die sie für sich erledigen, während Ihre Anwendung weiterläuft. Und weil es mehrere sind, dauert es kürzer.
Was es macht, in einem Satz
FoxCore startet eigene Kopien von Visual FoxPro, schickt ihnen Ihren Code genau so, wie er geschrieben ist, und gibt Ihnen das Ergebnis zurück.
Was an dieser Zeichnung für jemanden zählt, der seit Jahren FoxPro schreibt: der Code, der dort drin läuft, ist ganz normales FoxPro. Nichts in einer anderen Sprache neu zu schreiben, keine Threads zu lernen, keinen Dienst einzurichten. Es ist dasselbe SCAN ... ENDSCAN, das Sie schon haben, nur in einem anderen Prozess.
Und für jemanden, der ein FoxPro-System geerbt hat und aus der heutigen Welt kommt: Multithreading gibt es hier nicht, weil VFP es nicht hat. Was es gibt, ist Parallelität über Prozesse des Betriebssystems, gesteuert aus Ihrem Hauptprozess.
Das Wenigste, was zu schreiben ist
LOCAL loRt, lcId
loRt = CREATEOBJECT("FoxCore.Runtime")
loRt.Activate(lcMeineLizenz) && die Lizenz, als Zeichenkette
loRt.Attach(_VFP) && haengt die Engine an Ihr Programm
loRt.MaxWorkers = 4 && hoechstens vier Prozesse
lcId = loRt.RunScript("RETURN 2 + 2", "die erste")
DO WHILE !loRt.IsCompleted(lcId)
loRt.Pump() && ohne das kommt nichts an
ENDDO
? loRt.GetState(lcId), loRt.GetResult(lcId) && Completed 4
loRt.Shutdown()RunScript() bekommt eine Zeichenkette mit Ihrem Code und liefert eine Kennung zurück. Wenn der Aufruf zurückkommt, läuft die Aufgabe bereits dort draußen. Das Ergebnis ist das, was Ihr Code mit RETURN zurückgibt.
Die drei Dinge, die man verstehen muss
Wenn Sie aus diesem Artikel nur einen Abschnitt mitnehmen, dann diesen.
1. Der arbeitende Prozess sieht nichts von Ihrem. Er wird sauber geboren: keine Ihrer Variablen (auch keine PUBLIC), keine geöffneten Tabellen, keine Cursor, kein SET DEFAULT, keine Datensitzung. Er öffnet selbst, was er braucht. Was wie ein Nachteil aussieht, ist das Beste daran, denn eine Aufgabe, die abstürzt, kann Ihre Sitzung nicht beschädigen, und acht Aufgaben gleichzeitig kommen sich nicht in die Quere. Code, der im Befehlsfenster funktioniert, kann als Aufgabe scheitern, und fast immer aus diesem Grund: dort hat er Ihre Umgebung geerbt.
2. Pump() ist das, was die Meldungen durchlässt. VFP erledigt eine Sache nach der anderen, auch beim Empfangen. Die Engine betritt Ihr Programm nie von sich aus: sie stellt sich an und wartet, bis Sie ihr Platz machen, und dieser Platz ist Pump(). Ohne das kommen weder Ergebnisse noch Fortschritt an. In einem Formular gehört Pump() in einen Timer, in einen unbeaufsichtigten Lauf in die Warteschleife.
3. Shutdown() schließt die Prozesse. Wenn Sie es nicht aufrufen, bleiben Prozesse offen, die niemand sieht und die den Speicher des Arbeitsplatzes belegen. Es gehört ans Ende, in den Fehlerpfad und in das Destroy des Formulars.
Was sich damit in einer Unternehmensanwendung machen lässt
Was jetzt kommt, sind Arbeiten, die gut passen, mit dem Grund dafür.
Fakturierung, Neuberechnungen, Abschlüsse. Dreihundert Rechnungen, verteilt auf vier oder acht Prozesse. Jede Aufgabe öffnet die Tabellen, die sie braucht, in SHARED, erledigt ihren Teil und liefert das Ergebnis zurück. Der Anwender sieht den Balken laufen und kann abbrechen.
Dokumente erzeugen. PDF, Excel, .frx-Berichte in eine Datei. Jedes Dokument ist vom vorherigen unabhängig, also lässt sich das ohne viel Nachdenken aufteilen.
Große Dateien importieren und exportieren. Ein Abschnitt pro Aufgabe. Die, die scheitert, hinterlässt ihren Fehler schriftlich, die übrigen laufen weiter.
Aufrufe an Webdienste, an das Finanzamt, an eine Bank. Hier vergeht die Zeit mit Warten auf eine Antwort. Warten in einem anderen Prozess kostet Ihren Bildschirm nichts, und mit acht Aufgaben sind es acht Wartezeiten gleichzeitig.
Der Nachtlauf, den Sie auch tagsüber starten können wollen. Derselbe Code taugt für beides, denn er ist ganz normaler FoxPro-Text.
Wie Sie ihm Daten übergeben und wie er Ihnen sagt, wo er steht
Da der arbeitende Prozess nichts von Ihrem sieht, gibt es drei Wege, ihm zu geben, was er braucht, vom kleinsten aufwärts: in den Text des Codes selbst einbauen, auf einer Tafel hinterlegen, die er liest, oder in einer DBF ablegen, die er öffnet. Die Tafel wird am häufigsten für Konfiguration, Pfade, das Buchungsjahr oder den angemeldeten Anwender genutzt.
* in Ihrer Anwendung
loRt.Publish("jahr", 2026)
* im Code, der getrennt laeuft
* RETURN 'Jahr ' + TRANSFORM(runtime.GetValue('jahr'))Diese Variable runtime legt die Engine in jede Aufgabe, und sie ist die einzige Tür zurück zu Ihnen. Damit sagt Ihnen der arbeitende Code, wie es läuft, schreibt in Ihr Protokoll und fragt, ob Sie den Abbruch angeordnet haben:
* im Code, der getrennt laeuft
runtime.Progress(i * 20, 'Dokument ' + TRANSFORM(i) + ' von 5')
runtime.Log('info', 'ohne Besonderheiten beendet')
IF runtime.IsCancelled()
EXIT
ENDIFAuf Ihrer Seite holen Sie das ab, indem Sie fragen (GetProgress), oder indem Sie ein eigenes Objekt registrieren, das die Engine aufruft. Und weil diese Aufrufe Sie innerhalb Ihres Pump() erreichen, also in Ihrem eigenen Thread, darf dieses Objekt ohne besondere Vorsicht einen Balken bewegen, in einen Cursor schreiben oder neu zeichnen. Zum Anhalten einer Aufgabe gibt es Cancel(), das wartet, bis der Code nachfragt, und Kill(), das den Prozess hart beendet, wenn der Code gar nicht nachfragt.
Die Rechnungsnummer, ohne Sperren
Das ist die häufigste Frage, und hier wird der Entwurf verständlich. Wenn acht Prozesse gleichzeitig Rechnungen erstellen, wer vergibt dann die fortlaufende Nummer? Die Antwort: Ihre Anwendung vergibt sie weiterhin, und die Aufgabe fragt danach:
DEFINE CLASS Nummernkreis AS Custom
nLetzte = 0
FUNCTION Naechste(tcSerie)
THIS.nLetzte = THIS.nLetzte + 1
RETURN TRANSFORM(tcSerie) + "-" + PADL(THIS.nLetzte, 4, "0")
ENDFUNC
ENDDEFINELOCAL loNummern
* in Ihrer Anwendung, bevor die Aufgaben starten
loNummern = CREATEOBJECT("Nummernkreis")
loRt.RegisterHandler("NaechsteNummer", loNummern, "Naechste")
* und im Code, der getrennt laeuft, dort wo nummeriert wird
* lcNummer = runtime.Call('NaechsteNummer', 'RE')
* RETURN lcNummerKeine doppelte Nummer und kein einziges FLOCK(). Diese Aufrufe werden in Ihrem Prozess bedient, innerhalb von Pump(), einer nach dem anderen, also gibt es nie zwei gleichzeitig und nichts zu sperren.
Was Ihr Rechner braucht und was der des Kunden
Hier kommt die ernste Frage, und es hilft, vorher zu erklären, wie die Sache darunter funktioniert.
Windows führt eine Art Notizbuch, in dem steht, welches Programm auf welchen Namen hört. Wenn Ihr Code CREATEOBJECT("Word.Application") ausführt, schaut Windows in dieses Notizbuch, sieht, welche ausführbare Datei diesen Namen bedient, und startet sie. Etwas in dieses Notizbuch einzutragen, nennen wir registrieren, und genau das tun Installationsprogramme.
FoxCore nutzt dieses Notizbuch zweimal. Einmal für die Engine, eine .dll, die bei der Installation registriert wird. Und einmal für die Prozesse, die die Arbeit machen, denn auch die werden über ihren Namen im Notizbuch gestartet. Der Name, der ab Werk verwendet wird, ist der von Visual FoxPro 9:
loRt.WorkerProgId = "VisualFoxpro.Application.9"Das funktioniert auf Ihrem Rechner, auf dem VFP installiert ist. Auf dem Arbeitsplatz eines Kunden gibt es das nicht und kann es nicht geben, denn vfp9.exe ist das vollständige Produkt von Microsoft und darf nicht weitergegeben werden. Deshalb lässt sich dieser Name seit Version 3.1.0 gegen den Ihrer eigenen ausführbaren Datei tauschen:
loRt.WorkerProgId = "VisualFoxpro.Application.a" && VFP Advanced 10.x
loRt.WorkerProgId = "MeinErp.Server" && Ihre EXE, beim KundenDie gute Nachricht: Sie müssen mit ziemlicher Sicherheit nichts Neues ausliefern. Es kann dieselbe .exe Ihrer Anwendung sein, mit einer Klasse mehr darin. Ihre ausführbare Datei bringt die VFP-Laufzeit bereits mit, die weitergegeben werden darf, und sie startet mit Ihrer fertig eingerichteten Umgebung, ist also sogar schneller als ein nacktes FoxPro.
Das hier geht zum Arbeitsplatz des Kunden:
| Was | Was damit zu tun ist |
|---|---|
FoxCore.Runtime.dll und Nexum.dll | werden mit Ihrer Anwendung kopiert, die erste wird registriert; dafür gibt es ein Installationsprogramm |
Ihre gewohnte .exe | liefern Sie ohnehin aus; registriert wird sie mit ihre.exe /regserver |
| die Lizenz | steckt als Zeichenkette in Ihrem Code. Es gibt keine Datei zu kopieren |
Beide Registrierungen verlangen Administratorrechte, und das ändert für Sie nichts: Ihr Installationsprogramm verlangt sie schon. Was auf einem Arbeitsplatz nicht taugt, ist eine Registrierung «nur für diesen Benutzer»: es gibt Windows-Prozesse, die diese Registrierung nicht sehen, und dann kommt «Klasse nicht registriert», obwohl auf Ihrem Rechner alles läuft.
Was Ihre ausführbare Datei mitbringen muss, um als Helfer zu dienen
Vier Dinge, und keines davon ist lang. Sie finden alle in der Vorlage, die dem Paket beiliegt.
Eine OLEPUBLIC-Klasse in Ihrem Projekt. Dateiname und Klassenname ergeben zusammen den Namen im Notizbuch: meineapp.exe mit der Klasse MeinWorker ergibt meineapp.MeinWorker.
Drei von Hand geschriebene Methoden in dieser Klasse. Über sie spricht FoxCore mit ihr, und sie müssen geschrieben werden, weil das Application-Objekt von VFP die Prozessgrenze nicht überquert.
Die Umgebung im Init() dieser Klasse eingerichtet. Wenn Windows Ihre .exe über das Notizbuch startet, läuft das Hauptprogramm nicht. Wenn Sie dort Ihre SET PATH, Ihre Klassen und Ihre Verbindung einrichten, wird der Helfer leer geboren, und niemand sagt es Ihnen.
Ein ON ERROR in der ersten Zeile von Init(), das von keiner Variablen abhängt. Dieser Prozess hat kein Fenster: ein nicht behandelter Fehler öffnet einen Dialog, den niemand schließen kann, und der Prozess hängt dort fest. Gegen eine echte ausführbare Datei gemessen sind das 3,465 s pro Aufgabe mit dem Dialog gegenüber 0,131 s ohne ihn. Und die Behandlung darf sich nur auf Funktionen stützen, denn ein CLEAR ALL zerstört die Variablen, bevor überhaupt etwas scheitert.
So sieht die vollständige Klasse aus, die Sie in Ihr Projekt kopieren und mit ihm kompilieren:
DEFINE CLASS MeinWorker AS Custom OLEPUBLIC
*-- Hier, und nur hier, wird die Umgebung eingerichtet: das
*-- Hauptprogramm der EXE laeuft nicht, wenn Windows sie ueber
*-- ihren Namen startet.
FUNCTION Init
*-- Als Allererstes, und ohne Variablen: ein CLEAR ALL zerstoert
*-- sie, bevor etwas scheitert, und die Behandlung selbst wuerde
*-- dann abstuerzen.
ON ERROR STRTOFILE("ERR " + TRANSFORM(ERROR()) + ": " + MESSAGE() + ;
CHR(13) + CHR(10), ADDBS(SYS(2023)) + "meinworker-fehler.log", 1)
THIS.UmgebungVorbereiten()
RETURN .T.
ENDFUNC
*-- Ihre Umgebung: was Ihr Code braucht, um ohne Ihr Formular zu
*-- arbeiten. Wo Sie sind, sagt _VFP.ServerName (SYS(16) und
*-- SYS(2004) taugen in einer ueber COM gestarteten EXE nicht).
PROCEDURE UmgebungVorbereiten
LOCAL lcBasis
lcBasis = ADDBS(JUSTPATH(_VFP.ServerName))
SET TALK OFF
SET SAFETY OFF
SET EXCLUSIVE OFF
SET DELETED ON
SET DEFAULT TO (lcBasis)
SET PATH TO (lcBasis + "libs") ADDITIVE
* SET CLASSLIB TO (lcBasis + "libs\motor.vcx") ADDITIVE
* SET PROCEDURE TO (lcBasis + "libs\funktionen.fxp") ADDITIVE
ENDPROC
*-- Die drei Methoden, ueber die FoxCore mit dem Helfer spricht.
*-- Sie muessen geschrieben werden: sie werden nicht geerbt.
FUNCTION DoCmd(tcBefehl)
IF VARTYPE(tcBefehl) <> "C" OR EMPTY(tcBefehl)
RETURN .F.
ENDIF
EXECSCRIPT(tcBefehl)
RETURN .T.
ENDFUNC
FUNCTION SetVar(tcName, tuWert)
LOCAL lcName
IF VARTYPE(tcName) <> "C" OR EMPTY(tcName)
RETURN .F.
ENDIF
lcName = ALLTRIM(tcName)
IF TYPE(lcName) = "U"
PUBLIC &lcName
ENDIF
STORE tuWert TO (lcName)
RETURN .T.
ENDFUNC
FUNCTION Eval(tcAusdruck)
IF VARTYPE(tcAusdruck) <> "C" OR EMPTY(tcAusdruck)
RETURN .NULL.
ENDIF
RETURN EVALUATE(tcAusdruck)
ENDFUNC
ENDDEFINEKompiliert wird wie immer, und registriert wird einmal:
BUILD EXE meineapp FROM meineapp
meineapp.exe /regserverEine Sache kostet einen Nachmittag, wenn man sie nicht kennt: VFP erzeugt bei jeder Kompilierung eine neue Kennung für die Klasse, und dann muss erneut registriert werden. Abgeschaltet wird das im Kästchen «Regenerate Component IDs» des Erstellungsdialogs oder über das fünfte Argument von Project.Build().
Bevor Sie etwas einbauen, gibt es einen Befehl, der das prüft, indem er Ihre ausführbare Datei startet und darin einen Testcode ausführt, und der Ihnen sagt, wie lange sie bis zur Betriebsbereitschaft braucht:
? loRt.ProbeWorker("MeinErp.Server")Wenn das Starten mehr kostet als die Arbeit
Einen Prozess zu starten hat seinen Preis. Ein nacktes FoxPro braucht etwa eine Zehntelsekunde bis zur Betriebsbereitschaft; eine ausführbare Datei, die die Umgebung eines ERP mit offener Verbindung einrichtet, kann über eine Sekunde brauchen. Das sind die festen Kosten jeder Aufgabe, und daraus folgt die Faustregel: eine Aufgabe von zwei Zehntelsekunden lohnt sich nicht, weil das Starten mehr kostet als die Arbeit. Teilen Sie nach Beleg, nach Kunde oder nach Tabellenabschnitt auf, nicht nach Zeile.
Wenn Ihre Umgebung Zeit zum Einrichten braucht, können die Prozesse für die nächste Aufgabe bestehen bleiben:
loRt.MaxTasksPerWorker = 25 && ab Werk ist es 1
loRt.WorkerInitScript = lcUmgebungAufbauen && einmal pro Prozess
loRt.WorkerResetScript = "CLOSE DATABASES" && zwischen zwei AufgabenIn der Lasttest-Reihe gingen 120 Aufgaben auf acht Prozessen von 11,0 s und 120 Starts auf 3,3 s und 11 Starts zurück, nur durch das Ändern dieser Eigenschaft. Das Softwarehaus, das diese Version im August 2026 zertifiziert hat, maß mit seinem kompletten ERP im Helfer eine Verbesserung um das 3,2-fache.
Was es kostet: ein wiederverwendeter Prozess schleppt mit, was die vorherige Aufgabe hinterlassen hat, also ihre PUBLIC-Variablen, ihre geöffneten Tabellen und ihre SETs. Genau das hält die Umgebung am Leben, und deshalb räumt FoxCore von sich aus nichts auf. Was zwischen zwei Aufgaben geschlossen werden muss, schreiben Sie selbst in WorkerResetScript. Wer darüber nicht nachdenken möchte, lässt die Eigenschaft auf 1, und jede Aufgabe bekommt einen sauberen Prozess.
Wann es sich nicht lohnt
Das gehört genauso deutlich gesagt wie der Rest.
- FoxCore macht Ihren Code nicht schneller. Es schafft ihn aus dem Weg, was etwas anderes ist. Eine Verarbeitung von 40 s dauert weiter 40 s; was sich ändert, ist, dass die Anwendung nicht tot daliegt und dass acht davon in dieselbe Spanne passen.
- Wenn die Abfrage wegen eines fehlenden Index langsam ist, bleibt sie langsam.
- Jeder Prozess belegt Speicher und CPU. Vier oder acht auf einem normalen Arbeitsplatz, nicht vierzig.
- Das Ergebnis kommt als einfacher Wert zurück: Zeichen, Zahl, logisch, Datum. Ein VFP-Objekt überquert die Grenze nicht gut, also auf die Platte schreiben und den Pfad zurückgeben.
Was Sie nicht lernen müssen
Kein .NET, keine Threads, keine Windows-Dienste und keine Befehlszeile. Geschrieben wird FoxPro, und neu ist nur ein halbes Dutzend Aufrufe: RunScript, Pump, IsCompleted, GetResult, Cancel und Shutdown. Der Rest der Oberfläche ist da, wenn Sie ihn brauchen.
Die Zahlen in diesem Artikel stammen aus der Lasttest-Reihe und aus den beiden Zertifizierungen, die ein Softwarehaus im August 2026 mit seinem echten ERP durchgeführt hat, 47 Projekte im Produktivbetrieb bei Agrargenossenschaften: eine Obergrenze von 6,1 Aufgaben pro Sekunde bei minimalen Aufgaben, 5,97-mal schneller mit acht Prozessen bei Aufgaben von 2 s, und am Ende kein einziger verwaister Prozess.
Wo Sie anfangen
Das Paket bringt eine Demo mit, die mit DO Demo läuft, eine Anleitung in acht Schritten, von denen jeder in zwanzig Zeilen passt und einzeln ausgeführt werden kann, eine Rezeptsammlung mit vier vollständigen Fällen (eine Tabelle parallel, ein Formular mit Balken und Abbruchschaltfläche, ein Nachtlauf und die eigene EXE für den Arbeitsplatz des Kunden) und zwölf kurze Beispiele. Die Installation steht in der Dokumentation des Portals, der schnelle Einstieg im Quickstart.
Wenn Ihr Code CREATEOBJECT("FoxCore.FoxCoreClass") sagt, ist das die frühere Engine, die es weiter gibt und die weiter unterstützt wird. Dieser Artikel beschreibt die Linie 3.x, CREATEOBJECT("FoxCore.Runtime"), und die Entsprechungstabelle zwischen beiden steht am Ende der Referenz.