Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How many promises can Perl 6 keep?

That's a bit of a glib title, but in playing around with Promises I wanted to see how far I could stretch the idea. In this program, I make it so I can specify how many promises I want to make.

  • The default value in the thread scheduler is 16 threads (rakudo/ThreadPoolScheduler.pm)
  • If I specify more than that number, the program hangs but I don't get a warning (say, like "Too many threads").
  • If I set RAKUDO_MAX_THREADS, I can stop the program hanging but eventually there is too much thread competition to run.

I have two questions, really.

  • How would a program know how many more threads it can make? That's slightly more than the number of promises, for what that's worth.

  • How would I know how many threads I should allow, even if I can make more?

This is Rakudo 2017.01 on my puny Macbook Air with 4 cores:

my $threads = @*ARGS[0] // %*ENV<RAKUDO_MAX_THREADS> // 1;
put "There are $threads threads";

my $channel = Channel.new;

# start some promises
my @promises;
for 1 .. $threads {
    @promises.push: start {
        react {
            whenever $channel -> $i {
                say "Thread {$*THREAD.id} got $i";
                }
            }
        }
    }
put "Done making threads";

for ^100 { $channel.send( $_ ) }
put "Done sending";

$channel.close;

await |@promises;

put "Done!";
like image 267
brian d foy Avatar asked Apr 17 '17 10:04

brian d foy


1 Answers

This isn't actually about Promise per se, but rather about the thread pool scheduler. A Promise itself is just a synchronization construct. The start construct actually does two things:

  1. Ensures a fresh $_, $/, and $! inside of the block
  2. Calls Promise.start with that block

And Promise.start also does two things:

  1. Creates and returns a Promise
  2. Schedules the code in the block to be run on the thread pool, and arranges that successful completion keeps the Promise and an exception breaks the Promise.

It's not only possible, but also relatively common, to have Promise objects that aren't backed by code on the thread pool. Promise.in, Promise.anyof and Promise.allof factories don't immediately schedule anything, and there are all kinds of uses of a Promise that involve doing Promise.new and then calling keep or break later on. So I can easily create and await on 1000 Promises:

my @p = Promise.new xx 1000;
start { sleep 1; .keep for @p };
await @p;
say 'done' # completes, no trouble

Similarly, a Promise is not the only thing that can schedule code on the ThreadPoolScheduler. The many things that return Supply (like intervals, file watching, asynchronous sockets, asynchronous processes) all schedule their callbacks there too. It's possible to throw code there fire-and-forget style by doing $*SCHEDULER.cue: { ... } (though often you care about the result, or any errors, so it's not especially common).

The current Perl 6 thread pool scheduler has a configurable but enforced upper limit, which defaults to 16 threads. If you create a situation where all 16 are occupied but unable to make progress, and the only thing that can make progress is stuck in the work queue, then deadlock will occur. This is nothing unique to Perl 6 thread pool; any bounded pool will be vulnerable to this (and any unbounded pool will be vulnerable to using up all resources and getting the process killed :-)).

As mentioned in another post, Perl 6.d will make await and react non-blocking constructs; this has always been the plan, but there was insufficient development resources to realize it in time for Perl 6.c. The use v6.d.PREVIEW pragma provides early access to this feature. (Also, fair warning, it's a work in progress.) The upshot of this is that an await or react on a thread owned by the thread pool will pause the execution of the scheduled code (for those curious, by taking a continuation) and and allow the thread to get on with further work. The resumption of the code will be scheduled when the awaited thing completes, or the react block gets done. Note that this means you can be on a different OS thread before and after the await or react in 6.d. (Most Perl 6 users will not need to care about this. It's mostly relevant for those writing bindings to C libraries, or doing over systems-y stuff. And a good C library binding will make it so users of the binding don't have to care.)

The upcoming 6.d change doesn't eliminate the possibility of exhausting the thread pool, but it will mean a bunch of ways that you can do in 6.c will no longer be of concern (of note, writing recursive conquer/divide things that await the results of the divided parts, or having thousands of active react blocks launched with start react { ... }).

Looking forward, the thread pool scheduler itself will also become smarter. What follows is speculation, though given I'll likely be the one implementing the changes it's probably the best speculation on offer. :-) The thread pool will start following the progress being made, and use it to dynamically tune the pool size. This will include noticing that no progress is being made and, combined with the observation that the work queues contain items, adding threads to try and resolve the deadlock - at the cost of memory overhead of added threads. Today the thread pool conservatively tends to spawn up to its maximum size anyway, even if this is not a particularly optimal choice; most likely some kind of hill-climbing algorithm will be used to try and settle on an optimal number instead. Once that happens, the default max_threads can be raised substantially, so that more programs will - at the cost of a bunch of memory overhead - be able to complete, but most will run with just a handful of threads.

like image 88
Jonathan Worthington Avatar answered Sep 19 '22 17:09

Jonathan Worthington