GHC utiliza una especie de híbrido de multitarea cooperativa y preventiva en su implementación de concurrencia.
En el nivel de Haskell, parece preventivo porque los subprocesos no necesitan ceder explícitamente y pueden verse interrumpidos por el tiempo de ejecución en cualquier momento. Pero en el nivel de tiempo de ejecución, los subprocesos "ceden" cada vez que asignan memoria. Dado que casi todos los hilos de Haskell se asignan constantemente, esto generalmente funciona bastante bien.
Sin embargo, si un cálculo en particular puede optimizarse en un código que no se asigna, puede dejar de cooperar en el nivel de tiempo de ejecución y, por lo tanto, no tener prioridad en el nivel de Haskell. Como señaló @Carl, en realidad es la -fomit-yields
bandera, lo que implica -O2
que permite que esto suceda:
-fomit-yields
Le dice a GHC que omita las comprobaciones de almacenamiento dinámico cuando no se realiza ninguna asignación. Si bien esto mejora los tamaños binarios en aproximadamente un 5%, también significa que los subprocesos que se ejecutan en bucles ajustados no asignados no se evitarán de manera oportuna. Si es importante poder interrumpir siempre dichos subprocesos, debe desactivar esta optimización. Considere también volver a compilar todas las bibliotecas con esta optimización desactivada, si necesita garantizar la interrumpibilidad.
Obviamente, en el tiempo de ejecución de un solo subproceso (sin -threaded
marca), esto significa que un subproceso puede eliminar por completo todos los demás subprocesos. Menos obvio, lo mismo puede suceder incluso si compila -threaded
y usa +RTS -N
opciones. El problema es que un subproceso no cooperativo puede privar al programador de tiempo de ejecución . Si en algún momento el subproceso no cooperativo es el único subproceso programado actualmente para ejecutarse, se volverá ininterrumpible y el programador nunca se volverá a ejecutar para considerar la programación de subprocesos adicionales, incluso si pudieran ejecutarse en otros subprocesos O / S.
Si solo está tratando de probar algunas cosas, cambie la firma de fib
a fib :: Integer -> Integer
. Como Integer
causa la asignación, todo comenzará a funcionar nuevamente (con o sin -threaded
).
Si se encuentra con este problema en código real , la solución más fácil, con diferencia, es la sugerida por @Carl: si necesita garantizar la capacidad de interrupción de los subprocesos, se debe compilar el código -fno-omit-yields
, lo que mantiene las llamadas del planificador en código no asignado . Según la documentación, esto aumenta los tamaños binarios; Supongo que también conlleva una pequeña penalización de rendimiento.
Alternativamente, si el cálculo ya está dentro IO
, entonces explícitamente yield
en el bucle optimizado puede ser un buen enfoque. Para un cómputo puro, puede convertirlo a IO y yield
, aunque generalmente puede encontrar una manera simple de introducir una asignación nuevamente. En la mayoría de las situaciones realistas, habrá una manera de introducir solo unos "pocos" yield
o asignaciones, lo suficiente para que el hilo responda nuevamente, pero no lo suficiente como para afectar seriamente el rendimiento. (Por ejemplo, si tiene algunos bucles recursivos anidados, yield
o si fuerza una asignación en el bucle más externo).
MVar
indican que es susceptible a las condiciones de carrera. Tomaría esa nota en serio.