En quoi consiste donc cette nouvelle fonctionnalité ? Julia implémente une forme de parallélisme de tâches : tout morceau de code peut être marqué comme parallélisable ; lors de son exécution, il est lancé dans un autre fil d'exécution. Un ordonnanceur dynamique gère l'exécution de tous ces fils d'exécution. Par exemple, ce morceau de code calcule la suite de Fibonacci de manière récursive et très inefficace, mais surtout sur autant de processeurs que l'on souhaite (si on demande un élément suffisamment éloigné dans la suite) :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | import Base.Threads.@spawn function fib(n::Int) if n < 2 return n end t = @spawn fib(n - 2) return fib(n - 1) + fetch(t) end |
La chose importante à remarquer est @spawn : cette commande permet de lancer un bout de code sur un autre fil d'exécution. Le résultat n'est pas immédiatement retourné, mais bien une référence qui permet de le récupérer une fois qu'il est disponible (à travers fetch).
Derrière, l'ordonnanceur fait en sorte que l'exécution soit la plus efficace possible, peu importe le nombre de tâches qu'on lui demande d'exécuter. Il est prévu pour monter à plusieurs millions de tâches à effectuer, de telle sorte que le programmeur est vraiment libéré de tous les détails pratiques (lancer, synchroniser des fils d'exécution). L'implémentation fonctionne aussi de manière composante : à l'intérieur d'une tâche exécutée en parallèle, on peut appeler des fonctions qui, elles-mêmes, font appel à des tâches parallèles. Toutes ces tâches sont ordonnancées de manière efficace, de telle sorte qu'on ne doive pas se demander comment chaque fonction est codée.
Jusqu'à présent, en Julia, quand on appelait une fonction qui pouvait exploiter une forme de parallélisme à l'intérieur d'une section parallèle, on pouvait très vite remarquer une dégradation de la performance. L'explication est un conflit de ressources (oversubscription) : l'application veut se lancer sur bien plus de cœurs que disponible. Par exemple, sur un ordinateur à quatre cœurs, on divise un gros problème en quatre morceaux équivalents, chacun devant résoudre un gros système linéaire : cette dernière opération peut également se paralléliser de manière efficace ; jusqu'à présent, avec Julia (ou OpenMP, ou encore bon nombre d'autres environnements de programmation parallèles), seize fils d'exécution se chargent de résoudre quatre systèmes linéaires — sur quatre cœurs. Pour améliorer les temps de calcul, il vaut mieux alors désactiver le parallélisme au niveau des systèmes linéaires ! Le nouveau système, entièrement composable, gère ces situations en ne laissant que quatre fils s'exécuter en même temps.
Julia dispose aussi d'une série d'outils bien pratiques : des sémaphores, des verrous, du stockage par fil d'exécution (notamment utilisé pour la génération de nombres aléatoires : chaque fil dispose de son propre objet RNG).
En coulisses, ce système n'est pas d'une simplicité déconcertante à gérer. Les tâches de Julia ne sont pas implémentées de la même manière pour chaque système d'exploitation : Windows dispose de fibres, conceptuellement très proches, mais pas Linux (certaines bibliothèques implémentent ce genre de mécanisme, cependant). Chaque tâche nécessite sa propre pile d'appels de fonction pour gérer l'exécution, mais elle doit être gérée de manière particulière : le système d'exploitation se charge d'énormément de choses pour passer l'exécution d'un fil à l'autre, mais, en ce qui concerne les tâches, les applications doivent implémenter beaucoup de choses elles-mêmes. Pour le moment, Julia considère qu'une tâche ne peut s'exécuter que dans un seul fil d'exécution, sans possibilité de passer sur un autre fil pour mieux exploiter le processeur, mais cela arrivera dans le futur.
L'implémentation actuelle est complète d'un point de vue Julia, mais a une limitation majeure : les bibliothèques appelées par Julia (par exemple, pour la résolution de systèmes linéaires) sont écrites en C et ne gèrent pas nativement cette forme de parallélisme. Ce sera corrigé dans une version à venir. L'API n'est pas encore définitive et pourrait légèrement évoluer avant d'être entièrement stabilisée. La gestion des entrées-sorties n'est pas encore au point : un verrou global est imposé sur ces opérations, mais cela devrait changer. Une fois ces éléments améliorés, la bibliothèque standard pourrait commencer à exploiter le parallélisme de tâches de manière interne, par exemple pour trier des tableaux suffisamment grands.
Source : Announcing composable multi-threaded parallelism in Julia.