multithreading - ¿Cuántas promesas puede mantener Perl 6?
promise perl6 (2)
Es un título un poco flibante, pero al jugar con Promises quería ver hasta dónde podía estirar la idea. En este programa, lo hago para poder especificar cuántas promesas quiero hacer.
- El valor predeterminado en el programador de hilos es 16 hilos ( rakudo / ThreadPoolScheduler.pm )
- Si especifico más que ese número, el programa se bloquea pero no aparece una advertencia (por ejemplo, "Demasiados hilos").
- Si configuro RAKUDO_MAX_THREADS, puedo detener la ejecución del programa, pero finalmente hay demasiada competencia de subprocesos para ejecutar.
Tengo dos preguntas, de verdad.
¿Cómo sabría un programa cuántos hilos más puede hacer? Eso es un poco más que el número de promesas, por lo que eso vale.
¿Cómo sabría cuántos hilos debería permitir, incluso si puedo hacer más?
Este es Rakudo 2017.01 en mi diminuta Macbook Air con 4 núcleos:
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!";
use v6.d.PREVIEW;
rápida, agregue el use v6.d.PREVIEW;
en la primera linea
Esto soluciona una serie de problemas de agotamiento de hilos.
Agregué algunos otros cambios como $*SCHEDULER.max_threads
y agregué el "id" de Promesa para que sea fácil ver que el id. Del Subproceso no se correlaciona necesariamente con una Promesa dada.
#! /usr/bin/env perl6
use v6.d.PREVIEW; # <--
my $threads = @*ARGS[0] // $*SCHEDULER.max_threads;
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!";
There are 16 threads
Done making threads
Thread 4 (14) got 0
Thread 4 (14) got 1
Thread 8 (8) got 3
Thread 10 (6) got 4
Thread 6 (1) got 5
Thread 16 (5) got 2
Thread 3 (16) got 7
Thread 7 (8) got 8
Thread 7 (9) got 9
Thread 5 (3) got 6
Thread 3 (6) got 10
Thread 11 (2) got 11
Thread 14 (5) got 12
Thread 4 (16) got 13
Thread 16 (15) got 14 # <<
Thread 13 (11) got 15
Thread 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 22
Thread 18 (15) got 23 # <<
Thread 18 (15) got 24 # <<
Thread 8 (13) got 25
Thread 7 (15) got 26 # <<
Thread 3 (15) got 27 # <<
Thread 7 (15) got 28 # <<
Thread 8 (15) got 29 # <<
Thread 13 (13) got 30
Thread 14 (13) got 31
Thread 8 (13) got 32
Thread 6 (13) got 33
Thread 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 39
Thread 3 (15) got 40 # <<
Thread 7 (14) got 41
Thread 12 (15) got 42 # <<
Thread 15 (15) got 43 # <<
Thread 4 (1) got 44
Thread 11 (1) got 45
Thread 7 (15) got 46 # <<
Thread 8 (15) got 47 # <<
Thread 7 (15) got 48 # <<
Thread 17 (15) got 49 # <<
Thread 10 (10) got 50
Thread 10 (15) got 51 # <<
Thread 11 (14) got 52
Thread 6 (8) got 53
Thread 5 (13) got 54
Thread 11 (15) got 55 # <<
Thread 11 (13) got 56
Thread 3 (13) got 57
Thread 7 (13) got 58
Thread 16 (16) got 59
Thread 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 65
Thread 15 (15) got 66 # <<
Thread 17 (15) got 67 # <<
Thread 11 (13) got 68
Thread 10 (15) got 69 # <<
Thread 3 (15) got 70 # <<
Thread 11 (15) got 71 # <<
Thread 6 (15) got 72 # <<
Thread 16 (13) got 73
Thread 6 (13) got 74
Thread 17 (15) got 75 # <<
Thread 4 (13) got 76
Thread 8 (13) got 77
Thread 12 (15) got 78 # <<
Thread 6 (11) got 79
Thread 3 (15) got 80 # <<
Thread 11 (13) got 81
Thread 7 (13) got 82
Thread 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 88
Thread 3 (13) got 89
Thread 18 (13) got 90
Thread 6 (13) got 91
Thread 18 (13) got 92
Thread 15 (15) got 93 # <<
Thread 16 (15) got 94 # <<
Thread 12 (15) got 95 # <<
Thread 17 (15) got 96 # <<
Thread 11 (13) got 97
Thread 15 (16) got 98
Thread 18 (7) got 99
Done sending
Done!
En realidad, esto no se trata de Promise
per se, sino más bien del programador del grupo de subprocesos. Una Promise
sí misma es solo una construcción de sincronización. La construcción de start
realmente hace dos cosas:
- ¡Garantiza
$_
,$/
y$!
nuevos$!
dentro del bloque - Llamadas
Promise.start
con ese bloque
Y Promise.start
también hace dos cosas:
- Crea y devuelve una
Promise
- Programa el código en el bloque que se ejecutará en el grupo de subprocesos, y organiza que la finalización exitosa conserve la
Promise
y una excepción rompa laPromise
.
No solo es posible, sino que también es relativamente común, tener objetos Promise
que no estén respaldados por código en el grupo de subprocesos. Promise.in
, Promise.anyof
y Promise.allof
fábricas no programan nada inmediatamente, y hay todo tipo de usos de una Promise
que implica hacer Promise.new
y luego llamar a keep
o break
más adelante. Entonces puedo crear y await
fácilmente en 1000 Promise
s:
my @p = Promise.new xx 1000;
start { sleep 1; .keep for @p };
await @p;
say ''done'' # completes, no trouble
Del mismo modo, una Promise
no es lo único que puede programar código en ThreadPoolScheduler
. Las muchas cosas que devuelven el Supply
(como intervalos, visualización de archivos, conectores asíncronos, procesos asíncronos) también programan sus devoluciones de llamadas allí. Es posible lanzar código allí, disparando y olvidando el estilo haciendo $*SCHEDULER.cue: { ... }
(aunque a menudo te importa el resultado, o cualquier error, por lo que no es especialmente común).
El planificador de grupo de subprocesos Perl 6 actual tiene un límite superior configurable pero forzado, que tiene un valor predeterminado de 16 subprocesos. Si crea una situación en la que los 16 están ocupados pero no puede avanzar, y lo único que puede progresar es atascarse en la cola de trabajo, entonces se producirá un bloqueo. Esto no es exclusivo del grupo de subprocesos Perl 6; cualquier grupo limitado será vulnerable a esto (y cualquier grupo ilimitado será vulnerable al uso de todos los recursos y a la muerte del proceso :-)).
Como se mencionó en otra publicación, Perl 6.d hará await
y react
construcciones que no bloqueen; este siempre ha sido el plan, pero no había suficientes recursos de desarrollo para realizarlo a tiempo para Perl 6.c. El use v6.d.PREVIEW
pragma proporciona acceso temprano a esta característica. (También, advertencia justa, es un trabajo en progreso). El resultado de esto es que await
o react
en un hilo propiedad del grupo de subprocesos detendrá la ejecución del código programado (para los curiosos, tomando una continuación) y y permita que el hilo continúe con más trabajo. La reanudación del código se programará cuando se complete lo esperado o se done
bloque de react
. Tenga en cuenta que esto significa que puede estar en una secuencia de sistema operativo diferente antes y después de await
o react
en 6.d. (La mayoría de los usuarios de Perl 6 no tendrán que preocuparse por esto. Es principalmente relevante para quienes escriben enlaces a bibliotecas C, o para cosas sobre sistemas. Y una buena vinculación de biblioteca C lo hará para que los usuarios de la vinculación no tengan importar.)
El próximo cambio 6.d no elimina la posibilidad de agotar el grupo de subprocesos, pero significará que un montón de maneras que puede hacer en 6.c ya no serán motivo de preocupación (de nota, escribir concursos recursivos / dividir cosas que await
los resultados de las partes divididas, o tener miles de bloques reactivos activos lanzados con start react { ... }
).
De cara al futuro, el programador del grupo de subprocesos también se volverá más inteligente. Lo que sigue es especulación, aunque dado que es probable que sea el que implementa los cambios, es probablemente la mejor especulación que se ofrece. :-) El grupo de subprocesos comenzará a seguir el progreso realizado y lo usará para ajustar dinámicamente el tamaño de la agrupación. Esto incluirá notar que no se está progresando y, combinado con la observación de que las colas de trabajo contienen elementos, agregar hilos para tratar de resolver el interbloqueo, a costa de la sobrecarga de memoria de los subprocesos agregados. Actualmente, el grupo de subprocesos conservadoramente tiende a engendrar hasta su tamaño máximo de todos modos, incluso si esta no es una opción particularmente óptima; lo más probable es que se use algún tipo de algoritmo de alpinismo para intentar establecer un número óptimo. Una vez que eso sucede, el valor predeterminado de max_threads puede elevarse sustancialmente, de modo que se puedan completar más programas, a costa de un montón de gastos generales de memoria, pero la mayoría se ejecutará con solo un puñado de subprocesos.