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_handleris 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 asstartblocks, the.uncaught_handlerwill never be used because they each provide their own exception handling. -
The
.cuemethod 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
.loadsmethod 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:
-
ThreadPoolScheduleris 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.cuereturns, the tasks may not have started yet. The.cancelmethod of the returned object may be used to request cancelling the work of a given task. -
CurrentThreadScheduleris an alternate scheduler. It basically just executes the task immediately and returns after the task is complete. The returned cancellation object has a.cancelmethod, 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.