Introduction
The Dogelog Player is a Prolog system written 100% in Prolog that targets the Python and the JavaScript platform. It features a Prolog engine that can suspend its execution, either on the occasion of an explicit '$YIELD'/1 call or of an implicit auto-yield, in case the later is enabled.
Sofar this feature was only used for a single task such as a command line Prolog top-level or some graphic applications in the browser. We extended the Dogelog Player by two primities call_later/2 and create_task/1 so as to allow the non-preemptive execution of multiple tasks in a single thread.
Stackless Coroutines
The Prolog engine of the Dogelog Player is dual use. It can be called from within an async routine and it might return promise requests, which the async routine might then handle via some await. Or it can be called from an ordinary routine, considering a promise request an error.
For stackless coroutines we interpreted the notion of a "callback" as an ordinary routine call into the Dogelog Player. Subsequently auto-yielding or network I/O based on promises will not work for such a "callback". The two new primitive reads as follows:
-
call_later(G, T):
The built-in schedules the goal G to be executed after T milliseconds. -
create_task(G):
The built-in schedules the goal G to be executed.
Only the primitive call_later/2 deals with "callback"s, and it will execute the callback on the main stack of the Prolog engine. In as far "callback"s will also share the garbage collection of the main stack. The "callback" will contribute to a threshold that trigger the aperiodic garbage collection.
Stackfull Fibers
The primitive create_task/1 deals with "task"s. "Task"s are allowed to make promise requests and they are allowed to run in auto-yielding mode. For tasks we introduced sides stacks with their own garbage collection. The side stacks share a counter which triggers the periodic garbage collection.
Coroutines are often discussed based on their cost of context switching. We made some calculations and found that context switching is probably not the dominant cost. If we take our typical auto-yield configuration, we will have only 60 context switches per second.
These context switches have to be viewed vis à vis the cost of executing the Prolog engine itself. Many designs therefore choose the path with a Prolog engine object. Against all odds, we stayed with the global state design of the Dogelog Player and do simply switch this state into an object.
Command Line
The two new primitives are now available for both platforms, Python and JavaScrpt. The execution of multitasked examples from the commandline of the Prolog top-level is currently a little bit tricky, since the commandline still uses a blocking read on both platforms.
Concerning the "callback" primitive our runnning example consists of two coroutines "tick" and "tock". The coroutine tick is required to write tick every 1 second, whereas the couroutine tock is required to write tock every 5 seconds. We simply post continuation "callback"s via call_later/2:
tick(0) :- !.
tick(N) :- M is N-1, write('tick '), flush_output,
call_later(tick(M), 1000).
tock(0) :- !.
tock(N) :- M is N-1, write('tock '), flush_output,
call_later(tock(M), 5000).
To give the command line the chance to show the result, we use sleep(12000):
Browser Sandbox
For the JavaScript platform there are the choices nodeJS and the browser. Both choices are already supported by the Dogelog Player. In the browser the predicate create_task/1 will have a delay of 4ms since we did not yet figure out how to bring a setImmediate() to the browser and only use setTimeout().
When using "task"s we are not bound to the continuation style. The Prolog engine of the Dogelog Player allows yielding anywhere, also inside backtracking. We therefore can code the example slightly differently with failure driven loop over the non-deterministic predicate between/3:
tick :- between(1,11,_), write('tick '), flush_output,
sleep(1000), fail; true.
tock :- between(1,3,_), write('tock '), flush_output,
sleep(5000), fail; true.
The browser shows us as expected:
Conclusions
We stayed with the global state design of the Dogelog Player but could nevertheless provide stackless and stackfull coroutines. Our running example is two text echoing coroutines "tick" and "tock", with different frequencies. Works fine on the Python platform and the JavaScript platforms.
Example 33: Stackless Multitasking
https://www.dogelog.ch/littab/moblet/docs/15_learning/02_kennel/example33/package.html
Example 34: Stackfull Multitasking
https://www.dogelog.ch/littab/doclet/docs/15_learning/02_kennel/example34/package.html