JavaScript's event loop uses a message queue to schedule work, and runs each message to completion before starting the next. As a result, a niche-but-surprisingly-common pattern in JavaScript code is to schedule a function to run after the messages currently in the queue have been processed using setTimeout(fn, 0)
. For example:
setTimeout(() => {console.log('first')}, 0);
console.log('second');
// OUTPUT: "second\nfirst"
(see MDN's description for more details.)
Does Raku's offer any similar way to schedule work immediately after all currently scheduled work is completed? Based on my understanding of Raku's concurrency model (mostly just from this 6guts post), it seems that Raku uses a similar message queue (though please correct me if that's wrong!). I initially thought that Promise.in(0).then: &fn
was a direct equivalent:
my $p = Promise.in(0).then: { say 'first' }
say 'second';
await $p;
# OUTPUT: «second\nfirst» # ...usually
However, after running the above code many times, I realized that it's just setting up a race condition and 'first'
is sometimes first. So, is there any Raku code that does provide the same behavior? And, if so, is that behavior a consequence of intentional semantics that Raku/Roast have decided on rather than a result of (perhaps temporary) implementation details?
The setInterval method has the same syntax as setTimeout : let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...) All arguments have the same meaning. But unlike setTimeout it runs the function not only once, but regularly after the given interval of time.
Invoking setTimeout with a callback, and zero as the second argument will schedule the callback to be run asynchronously, after the shortest possible delay - which will be around 10ms when the tab has focus and the JavaScript thread of execution is not busy.
To explain: If you call setTimeout() with a time of 0 ms, the function you specify is not invoked right away. Instead, it is placed on a queue to be invoked “as soon as possible” after any currently pending event handlers finish running.
The setTimeout() is executed only once. If you need repeated executions, use setInterval() instead. Use the clearTimeout() method to prevent the function from starting.
Raku doesn't have an ordered message queue. It has an unordered list of things that needs doing.
# schedule them to run at the same second
# just to make it more likely that they will be out of order
my $wait = now + 1;
my @run;
for 1..20 -> $n {
push @run, Promise.at($wait).then: {say $n}
}
await @run;
That could print the numbers in any order.
1
2
3
4
5
6
7
8
11
12
13
14
15
16
17
18
9
10
19
20
Raku is actually multi-threaded. If you give it enough work, it will use all of your cpu cores.
That means that there can never be a way to say run this after everything currently in the queue finishes.
Even if I just used start
, it could sometimes run things out of order.
my @run;
for 1..20 -> $n {
push @run, start {say $n}
};
await @run;
You could run that hundreds of times before it starts printing things out of order, but it will do so eventually.
Even if you went low-level and used $*SCHEDULER.cue
, there is no guarantee that it will run after everything else.
my @nums;
for 1..100 -> $n {
$*SCHEDULER.cue: {push @nums, $n; say $n}
}
say @nums;
Not only may it run out of order, the @nums
array probably won't have all of the values because each thread may clobber what another thread is doing.
Under the hood, the Promise methods that schedule something to run eventually calls $*SCHEDULER.cue
in some fashion.
You can tell Raku to run your code after something else.
my $p = Promise.in(1);
my $p2 = $p.then: {say 'first'}
my $p3 = $p.then: {say 'second'}
react {
whenever start say('first') {
whenever start say('second') {
}
}
}
You need to have a reference to that thing though.
If Raku did have a way to run things after the currently scheduled events, then it would have to keep track of what is running and make sure that your code doesn't run until after they have finished.
my $a = start {
# pointless busy-work that takes two seconds
my $wait = now + 2;
my $n = 0;
while now ≤ $wait {
$n++
}
say $n; # make sure the loop doesn't get optimized away
say 'first';
}
my $b = start say 'second';
await $a, $b;
second
1427387
first
If that made sure that $b
ran after $a
, then no work would be done on $b
for two whole seconds.
Instead it just causes $b
to run on another thread because the one that is dealing with $a
is currently busy.
That is a good thing, because what if $b
was also slow. We would be scheduling two slow things to run in sequence instead of in parallel.
I think that the only reason it currently works in Javascript is because it doesn't appear to take advantage of multiple cpu cores. Or it has something like a GIL.
I've written Raku code that has had my 4 core CPU at 500% utilization. (Intel hyperthreaded cpu, where one core appears to be 2 cores)
I'm not sure you can do same with a single Javascript program.
You can do something similar in a more explicit manner using a Channel:
# Subclass Channel for type safety.
class MessageQueue is Channel {
method send(&code) { nextsame }
method run { while self.poll -> &code { &code.() } }
}
# Our queue
my MessageQueue \message-queue .= new;
# Schedule everything with the queue, just for fun.
message-queue.send: {
# We can schedule code to run within scheduled code
message-queue.send: { say ‘first’ };
say ‘second’;
# Demonstrating type checking in the send call
try { message-queue.send: ‘Hello’; } or warn $!;
}
message-queue.run;
Just for fun, I created a PoC Scheduler that allows you to use run tasks through a single-thread channel using Promise.(in|at|start)
, see https://glot.io/snippets/fzbwj8me8w
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With