Raku equivalent to JavaScript's `setTimeout(fn, 0)`?
Asked Answered
S

2

9

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?

Shillong answered 24/5, 2021 at 21:46 Comment(0)
C
12

Unordered

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.

Schedule off something else

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.

Slow

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.

Javascript

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.

Cadmus answered 25/5, 2021 at 3:9 Comment(0)
U
1

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

Uboat answered 7/6, 2021 at 13:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.