Programmation asynchrone en Ruby : présentation de la gem concurrent-ruby (Partie 3)

Après avoir découvert les outils natifs en Ruby, nous allons passer en revue la gem concurrent-ruby. Cette gem affirme être la seule avec un modèle de gestion de la mémoire réalisant des opérations atomiques, garantissant un comportement thread-safe pour toutes ses fonctionnalités.

concurrent-ruby est principalement un framework permettant de manipuler des threads de différentes saveurs.

Le module Async

« Lorsque ce module est inclus dans une classe, les objets de la classe deviennent intrinsèquement asynchrones. Chaque objet obtient son propre thread d’arrière-plan dans lequel réaliser ses appels de méthodes asynchrones. Les appels de méthodes asynchrones sont exécutés un par un dans l’ordre où ils sont reçus. »

require "concurrent"
 
class Demonstration
  include Concurrent::Async
 
  def dire_3s_plus_tard(phrase)
    sleep 3
    puts phrase
  end
end
 
demo = Demonstration.new
 
# Exemple synchrone, non thread-safe
demo.dire_3s_plus_tard("Premier")
puts "Second"
 
# Exemple asynchrone, non bloquant, thread-safe
demo.async.dire_3s_plus_tard("Second")
puts "Premier"
 
# Exemple synchrone, bloquant, thread-safe
demo.await.dire_3s_plus_tard("Premier")
puts "Second"

« Note importante : la garantie thread-safe tient seulement quand les appels de méthodes asynchrones ne sont pas mélangés à des appels directs aux méthodes. Utilisez uniquement des appels directs aux méthodes quand l’objet est utilisé exclusivement par un seul processus. Utilisez uniquement async et await quand l’objet est partagé entre plusieurs threads. »

La classe Future

« Un objet Future représente la promesse d’achever une action à un moment donné dans l’avenir. L’idée derrière une Future est d’envoyer une opération dans une exécution asynchrone, continuer à faire d’autres choses, puis retourner et récupérer le résultat de l’opération asynchrone à un moment ultérieur. »

require 'concurrent'
require 'open-uri'
 
class Crawler
  # Ma méthode qui prend du temps...
  def lire_mon_ip_publique
    uri = "http://canihazip.com/s"
    html = open(uri) {|f| f.collect{|line| line.strip } }
    html[0] # parce que open-uri renvoie un tableau
  end
end
 
ma_future = Concurrent::Future.execute{ Crawler.new.lire_mon_ip_publique }
ma_future.state #=> :pending
ma_future.pending? #=> true
ma_future.value(0) #=> nil (ne bloque pas)
 
sleep(3) # faire d'autres trucs
 
ma_future.state #=> :fulfilled
ma_future.fulfilled? #=> true
ma_future.value #=> "8.8.8.8"

Notez qu’on peut aussi appeler Future.new à la place de Future.execute et .execute plus tard sur l’objet pour en différer le départ. Les Futures ont différents états tout au long de leur vie (:unscheduled, :pending, :processing, :rejected et :fulfilled) et sont considérées complétées lorsque l’état est soit :rejected soit :fulfilled.

Il existe même un outil dataflow permettant d’exécuter du code uniquement lorsque plusieurs Futures sont considérées complétées.

La classe Promise

C’est une implémentation complètement inspirée de JavaScript. « Les Promises sont semblables aux Futures et partagent beaucoup des mêmes comportements. Les Promises sont cependant beaucoup plus robustes. Elles peuvent être chaînées dans une structure en arbre où chaque Promise peut avoir zéro ou plus d’enfants. Les Promises sont chaînées en utilisant la méthode then. Le résultat d’un appel à then est toujours une autre Promise. Les Promises sont résolues de manière asynchrone (par rapport au fil principal) mais dans un ordre strict : les parents sont garantis d’être résolus devant leurs enfants, les enfants avant leurs frères et sœurs plus jeunes. La méthode then prend deux paramètres: un bloc facultatif à exécuter lors de la résolution du parent et un appel facultatif à exécuter en cas d’échec du parent. Le résultat de chaque promesse est transmis à chacun de ses enfants jusqu’à la résolution de l’arbre. Quand une promesse est rejetée, tous ses enfants seront rejetés et recevront la raison. »

Il faut reconnaitre que c’est touffu. En même temps ça fait plus de choses que les Futures :

require 'concurrent'
 
ma_promesse = Concurrent::Promise.new{10}.then{ |x| x * 2 }.then{ |resultat| sleep 5; resultat + 10 }
ma_promesse.execute # comme pour les Futures, c'est quand on veut
ma_promesse.state #=> :pending
sleep 3
ma_promesse.state #=> :pending
sleep 3
ma_promesse.state #=> :fulfilled
ma_promesse.value #=> 30
ma_promesse.complete? #=> true

Sachez que le retour de la dernière opération d’un then sera passé à la Promise suivante, attention inline.

ma_promesse = Concurrent::Promise.execute{ raise StandardError.new("J'ai tout cassé !") }
sleep(0.1)
 
ma_promesse.state     #=> :rejected
ma_promesse.rejected? #=> true
ma_promesse.reason    #=> "#<StandardError: J'ai tout cassé !>"

Les états et les méthodes des Promises sont semblables à ceux des Futures (:unscheduled, :pending, :processing, :rejected et :fulfilled, .complete? etc).

Et au delà

Il existe tout un tas d’outils que je vais me garder de présenter en détail, ce serait trop long. Il existe des classes pour planifier des tâches et appeler du code périodiquement, des structures de données thread-safe (array, hash, map, tuple et une tonne d’autres) et plus encore.

La gem concurrent-ruby-edge va encore plus loin avec des fonctionnalités instables qui sont en train d’être regroupées dans un framework. Mais celle là ne sera pas prête d’être prod-ready avant un moment.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.