How many promises can Perl 6 keep? How many promises can Perl 6 keep? multithreading multithreading

How many promises can Perl 6 keep?


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.


Quick fix, add use v6.d.PREVIEW; on the first line.
This fixes a number of thread exhaustion issues.

I added a few other changes like $*SCHEDULER.max_threads, and adding the Promise “id” so that it is easy to see that the Thread id doesn't necessarily correlate with a given Promise.

#! /usr/bin/env perl6use v6.d.PREVIEW; # <--my $threads = @*ARGS[0] // $*SCHEDULER.max_threads;put "There are $threads threads";my $channel = Channel.new;# start some promisesmy @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!";
There are 16 threadsDone making threadsThread 4 (14) got 0Thread 4 (14) got 1Thread 8 (8) got 3Thread 10 (6) got 4Thread 6 (1) got 5Thread 16 (5) got 2Thread 3 (16) got 7Thread 7 (8) got 8Thread 7 (9) got 9Thread 5 (3) got 6Thread 3 (6) got 10Thread 11 (2) got 11Thread 14 (5) got 12Thread 4 (16) got 13Thread 16 (15) got 14 # <<Thread 13 (11) got 15Thread 4 (15) got 16 # <<Thread 4 (15) got 17 # <<Thread 4 (15) got 18 # <<Thread 11 (15) got 19 # <<Thread 13 (15) got 20 # <<Thread 3 (15) got 21 # <<Thread 9 (13) got 22Thread 18 (15) got 23 # <<Thread 18 (15) got 24 # <<Thread 8 (13) got 25Thread 7 (15) got 26 # <<Thread 3 (15) got 27 # <<Thread 7 (15) got 28 # <<Thread 8 (15) got 29 # <<Thread 13 (13) got 30Thread 14 (13) got 31Thread 8 (13) got 32Thread 6 (13) got 33Thread 9 (15) got 34 # <<Thread 13 (15) got 35 # <<Thread 9 (15) got 36 # <<Thread 16 (15) got 37 # <<Thread 3 (15) got 38 # <<Thread 18 (13) got 39Thread 3 (15) got 40 # <<Thread 7 (14) got 41Thread 12 (15) got 42 # <<Thread 15 (15) got 43 # <<Thread 4 (1) got 44Thread 11 (1) got 45Thread 7 (15) got 46 # <<Thread 8 (15) got 47 # <<Thread 7 (15) got 48 # <<Thread 17 (15) got 49 # <<Thread 10 (10) got 50Thread 10 (15) got 51 # <<Thread 11 (14) got 52Thread 6 (8) got 53Thread 5 (13) got 54Thread 11 (15) got 55 # <<Thread 11 (13) got 56Thread 3 (13) got 57Thread 7 (13) got 58Thread 16 (16) got 59Thread 5 (15) got 60 # <<Thread 5 (15) got 61 # <<Thread 6 (15) got 62 # <<Thread 5 (15) got 63 # <<Thread 5 (15) got 64 # <<Thread 17 (11) got 65Thread 15 (15) got 66 # <<Thread 17 (15) got 67 # <<Thread 11 (13) got 68Thread 10 (15) got 69 # <<Thread 3 (15) got 70 # <<Thread 11 (15) got 71 # <<Thread 6 (15) got 72 # <<Thread 16 (13) got 73Thread 6 (13) got 74Thread 17 (15) got 75 # <<Thread 4 (13) got 76Thread 8 (13) got 77Thread 12 (15) got 78 # <<Thread 6 (11) got 79Thread 3 (15) got 80 # <<Thread 11 (13) got 81Thread 7 (13) got 82Thread 4 (15) got 83 # <<Thread 7 (15) got 84 # <<Thread 7 (15) got 85 # <<Thread 10 (15) got 86 # <<Thread 7 (15) got 87 # <<Thread 12 (13) got 88Thread 3 (13) got 89Thread 18 (13) got 90Thread 6 (13) got 91Thread 18 (13) got 92Thread 15 (15) got 93 # <<Thread 16 (15) got 94 # <<Thread 12 (15) got 95 # <<Thread 17 (15) got 96 # <<Thread 11 (13) got 97Thread 15 (16) got 98Thread 18 (7) got 99Done sendingDone!