Search code examples
javascriptmessage-queuerakuevent-looprakudo

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


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?


Solution

  • 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.