Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 270
codesections Avatar asked May 24 '21 21:05

codesections


People also ask

What is alternative to setTimeout?

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.

What happens when setTimeout is 0?

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.

What happens when if setTimeout () call with 0ms?

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.

Does setTimeout run forever?

The setTimeout() is executed only once. If you need repeated executions, use setInterval() instead. Use the clearTimeout() method to prevent the function from starting.


2 Answers

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.

like image 169
Brad Gilbert Avatar answered Nov 06 '22 18:11

Brad Gilbert


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

like image 41
Maniacs Thrift Jewels Avatar answered Nov 06 '22 18:11

Maniacs Thrift Jewels