Raku Schedulers
A large number of concurrency-oriented coding in Raku depends on the use of a
Scheduler
. Many async operations
depend on the default scheduler created by the VM at the start of runtime. You
can access this via the dynamic variable named
$*SCHEDULER
.
The most important feature of a Scheduler
is the .cue
method. Calling that
method with a code reference will schedule the work for execution. The type of
scheduler will determine what exactly that means.
That said, this is a low-level interface and you probably shouldn’t be calling
.cue
in most code. The best practice is to rely on high-level tools like
start
blocks which due this and construct a Promise
for you to monitor the
work.
Every scheduler provides three methods:
-
The
.uncaught_handler
is an accessor that returns a routine or can be set to a routine which is called whenever an exception is thrown by the scheduler and is not handled by the task code itself. If no handler is provided and a cued task throws an exception, the application will exit on that exception. If you use the high level concurrency tools, such asstart
blocks, the.uncaught_handler
will never be used because they each provide their own exception handling. -
The
.cue
method is used to add a task to the schedule. The scheduler will perform that task as resources allow (depending on how the scheduler operates). -
The
.loads
method returns an integer indicating the current load on the scheduler. This is an indication of the size of the current job queue.
So, you could build a very simple scheduler like this:
class MyScheduler does Scheduler {
method cue(&code, Instant :$at, :$in, :$every, :$times = 1; :&catch) {
sleep $at - now if $at && $at > now;
sleep $in if $in;
for ^$times {
code();
CATCH {
default {
if &catch {
catch($_);
}
elsif self.uncaught_handler {
self.uncaught_handler.($_);
}
else {
.throw;
}
}
}
sleep $every if $every;
}
class { method cancel() { } }
}
# We don't really queue jobs, so always return 0 for the load
method loads(--> 0) { }
}
This is somewhat similar to what the CurrentThreadScheduler
does.
Rakudo has two schedulers built-in:
-
ThreadPoolScheduler
is the usual default$*SCHEDULER
. When it is constructed, you can set the number of threads it is permitted to use simultaneously. It then manages a pool of threads and will schedule cued tasks on those threads. As tasks complete, freeing up threads, the next tasks will be scheduled to run. Tasks may run concurrently with this scheduler. When.cue
returns, the tasks may not have started yet. The.cancel
method of the returned object may be used to request cancelling the work of a given task. -
CurrentThreadScheduler
is an alternate scheduler. It basically just executes the task immediately and returns after the task is complete. The returned cancellation object has a.cancel
method, but it is a no op as the work will always have completed by the time the scheduler returns.
Many async methods, such as the
.start
method on
Promise
, take a named :scheduler
argument where you can pass a custom scheduler. In general, you can stick to
the default scheduler. Probably the most common adjustment to a scheduler would
be to change the number of threads in the thread pool or switch to using the
current thread scheduler under some circumstances. Chances are you will need to
do neither of these. And if you need something exotic, it may be reasonable
to define your own scheduler as well. Things to consider.
Cheers.