Rolf B: Debugging Web Worker in Firefox

Beitrag lesen

Hallo Michael_K,

ja, und ich weiß jetzt auch, warum.

Es ist eine Race-Condition. In performTasks erzeugst Du den Subworker und registriert auf dem Subworker einen onmessage-Handler. Dieser soll das Ende des Subworkers feststellen.

In runSubWorker sendest Du dann die Ende-Nachricht.

Die Besonderheit ist, dass Du das hinter einem await tust. Das ist lediglich Syntaxzucker für einen Callback, den die .then-Methode eines Promise bekommt, und es bedeutet: alles, was hinter await folgt, läuft nicht mehr im normalen Ablauf der async-Funktion, sondern als Callback in der Microtask-Queue.

Aus Sicht der Browser-Runtime hängt das Worker-Objekt des Subworkers daher bereits an einem seidenen Faden. Es wird nicht gelöscht, weil es noch eine Referenz auf den await-Callback in der Mikrotask-Queue gibt, aber sobald dieser Callback durch ist, war's das und der Collector schlägt zu.

Damit ist das Subworker-Objekt weg, und es gibt nichts mehr, was es hält. Die performTasks-Funktion, die es erzeugt hatte, ist längst zu Ende und der onmessage-Callback, den Du da registrierst hast, hängt am Subworker-Objekt. Aber wenn das Objekt weg ist, gibt's auch keine Eventbehandlung mehr. Und je nach dem, wann der GC zuschlägt, lebt der Subworker lang genug um die Nachricht zu verarbeiten, oder auch nicht. Und dann ist der Ablauf zu Ende. Hat dein Originalcode auch eine async-Funktion für die Subworker?

Ich nehme an, dass die JS Runtime nach dem regulären Ende einer Worker-Funktion irgendwie sicherstellt, dass der letzte pushMessage-Aufruf des Workers noch verarbeitet wird. Wenn dieser letzte pushMessage-Aufruf aber aus einem Microtask kommt, klappt das nicht mehr. Zumindest nicht im Firefox. Die V8 Engine von Chrome scheint diesen Fall besser zu berücksichtigen.

Ich habe deine await-Verzögerer mal durch eine For-Schleife mit 100 Millionen durchläufen ersetzt. Die dauert dann ca 50ms - und damit laufen 200 Subworker problemlos durch. Eine await-Verzögerung mit 50ms bricht zuverlässig nach ca 100-150 Workern ab.

Wenn ich wieder auf deine await-Verzögerung zurückgehe, aber dann nach dem postMessage des subWorkerFinished eine weitere await-Verzögerung von 50ms mache, dann gibt es noch 50ms lang eine Referenz auf den Subworker, und das reicht, um zuverlässig die Antwort verarbeiten zu können.

Deine Lösung, die Subworker in einer globalen Variablen zu halten, funktioniert auch.

Du müsstest den Subworker-Code übrigens in einen try/finally einschließen und die Ende-Message im finally-Teil senden, damit Du bei einem Error im Subworker immer noch eine Rückmeldung bekommen kannst.

Was Du übrigens nicht zu tun brauchst, ist subworker.terminate(). Das ist nur nötig wenn Du Subworker abbrechen willst. Aber wenn Du die subWorkerFinished-Nachricht bekommst, ist er eh zu Ende.

Rolf

--
sumpsi - posui - obstruxi