Akka — le modèle d'acteurs
Communication concurrente par messages, sans état partagé. ActorSystem, receive, sender, problème des philosophes. La concurrence sans verrou ni effet de bord.
📄 Ressources du cours
ActorCalcul / ActorPizza.
🎥 Vidéos recommandées
-
🇫🇷
Introduction to Akka Actors with Java
Akka · 09:40 · 50k vues — Johan Andrén from the Akka team at Lightbend shows a short live-coding introduction to using the new actor APIs in Akka 2.6 for Java applications. For more i...
-
🇫🇷
Using Actors (in Scala with Akka)
Mark Lewis · 05:59 · 5k vues — This video gives a brief description of a problem that you might consider tackling with actors to help you see how you could think about your own problem sol...
-
🇬🇧
Introduction to Akka Actors with Java
Akka · 09:40 · 50k vues — Johan Andrén from the Akka team at Lightbend shows a short live-coding introduction to using the new actor APIs in Akka 2.6 for Java applications. For more i...
1. Le modèle d'acteurs
- A son propre état privé, jamais partagé
- Reçoit des messages dans une boîte aux lettres (mailbox)
- Traite les messages un à un, séquentiellement
- Peut envoyer des messages à d'autres acteurs
- Peut créer de nouveaux acteurs enfants
Comme deux acteurs ne partagent jamais de mémoire et que chacun traite ses messages séquentiellement, il n'y a pas besoin de mutex. La concurrence émerge de l'asynchronicité des messages, pas de l'accès à des variables partagées.
Akka est le framework Scala/Java qui implémente ce modèle, inspiré directement d'Erlang (Ericsson, années 80).
2. ActorSystem — le conteneur
Un ActorSystem est le conteneur qui gère le cycle de vie de tous les acteurs. C'est le point d'entrée du framework.
import akka.actor._
object Main extends App {
val system = ActorSystem("MySystem") // creation du systeme
// Creation d'acteurs
val server = system.actorOf(Props[Server], "server")
val client = system.actorOf(Props[Client], "client")
// Envoi d'un message
client ! "start"
// Arret apres un delai
Thread.sleep(5000)
system.terminate()
}
system.actorOf(Props[T], "nom")crée un acteur de typeTavec un nom unique. Retourne unActorRef.- L'
ActorRefest une référence légère sur l'acteur. On l'utilise pour envoyer des messages — pas l'objet lui-même. system.terminate()arrête proprement tous les acteurs.
3. Définir un acteur
Un acteur est une classe qui extends Actor et implémente la méthode receive.
class MonActeur extends Actor {
def receive : Receive = {
case message => ...
}
}
Le type Receive est en réalité PartialFunction[Any, Unit]. La méthode receive est un pattern matching sur les messages.
Exemple complet — serveur pizza
import akka.actor.{Actor, ActorSystem, Props}
class PizzaServer extends Actor {
def receive : Receive = {
case "pizza" =>
println("[Serveur] Préparation d'une pizza...")
sender() ! "Votre pizza est prête !"
case _ =>
sender() ! "On ne peut pas vous aider !"
}
}
Au reçu de "pizza", le serveur affiche un message et répond à l'expéditeur via sender(). Au reçu de tout autre message, il renvoie une excuse polie.
4. Envoyer et recevoir des messages
Opérateur ! — fire-and-forget
destination ! message
Envoie le message dans la mailbox de destination et continue immédiatement (asynchrone). On dit tell ou bang. Pas de retour, pas d'attente.
client ! "start" // envoi simple, fire-and-forget
server ! Factorial(5) // envoi d'un message structure (case class)
sender() — répondre à l'expéditeur
Dans le corps de receive, sender() renvoie l'ActorRef de l'acteur qui a envoyé le message courant.
case Factorial(n) =>
val result = factorial(n)
sender() ! Result(result) // repondre a celui qui a demande
Définir les messages — case classes / objects
sealed trait Message + plusieurs case class ou case object pour les variantes. Le receive devient un pattern matching exhaustif.
sealed trait Request
case class Factorial(n : Int) extends Request
case class SumFirst(n : Int) extends Request
case class Result(value : BigInt)
L'usage de sealed + case classes apporte les bénéfices du CM3 : exhaustivité vérifiée, pattern matching propre, == sémantique, pas de new.
5. Retrouver un acteur — actorSelection
Quand un acteur veut parler à un autre dont il n'a pas la référence directe, il utilise un chemin :
val server = context.actorSelection("/user/serverName")
server ! "pizza"
ActorRef vs ActorSelection
ActorRef= référence directe, garantit que l'acteur existe à la créationActorSelection= sélection par chemin, peut pointer sur 0, 1 ou plusieurs acteurs ; résolu à chaque envoi
ActorRef quand possible (plus rapide et plus sûr).
Hiérarchie des chemins
/user/: racine des acteurs créés par l'utilisateur/user/server: un acteur créé avecsystem.actorOf(Props[…], "server")/user/server/child: un acteur créé parserveraveccontext.actorOf(…, "child")
6. Cycle de vie d'un acteur
Chaque acteur a des hooks qu'on peut surcharger pour initialiser ou nettoyer :
| Hook | Quand | Usage typique |
|---|---|---|
preStart() | Avant le premier message | Initialisation, envoi du premier message |
postStop() | Après l'arrêt | Cleanup, libération de ressources |
preRestart(reason, msg) | Avant un redémarrage | Sauvegarder l'état avant reset |
postRestart(reason) | Après le redémarrage | Restaurer l'état |
class Client(serverPath : String) extends Actor {
val server = context.actorSelection(serverPath)
override def preStart() : Unit = {
server ! Factorial(5) // envoi automatique au demarrage
server ! SumFirst(10)
}
def receive : Receive = {
case Result(value) =>
println(s"${self.path.name} a reçu : $value")
}
}
7. Pattern ask — attendre une réponse
Parfois on veut une réponse, pas juste envoyer-et-oublier. Le pattern ask renvoie un Future qui se résout quand l'acteur répond.
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
implicit val timeout : Timeout = 5.seconds
val future = server ? Factorial(10) // ? renvoie Future[Any]
future onComplete {
case scala.util.Success(Result(v)) => println(v)
case scala.util.Failure(e) => println(s"erreur : $e")
}
Future et un timeout. À utiliser seulement quand on a vraiment besoin de chaîner une réponse — préférer tell (!) dans tous les autres cas.
8. Exemple complet — ActorCalcul
Le projet du cours combine tout ce qu'on a vu : sealed trait de messages, serveur qui répond, deux clients qui interagissent en parallèle.
import akka.actor._
import scala.concurrent.duration._
import akka.util.Timeout
import akka.pattern.ask
import scala.concurrent.ExecutionContext.Implicits.global
// ===== Messages =====
sealed trait Request
case class Factorial(n : Int) extends Request
case class SumFirst(n : Int) extends Request
case class Result(value : BigInt)
// ===== Serveur =====
class Server extends Actor {
def factorial(n : Int) : BigInt =
if (n <= 1) 1 else n * factorial(n - 1)
def sumFirst(n : Int) : Int = (1 to n).sum
def receive : Receive = {
case Factorial(n) =>
println(s"Serveur: calcul factorielle de $n")
sender() ! Result(factorial(n))
case SumFirst(n) =>
println(s"Serveur: somme des $n premiers")
sender() ! Result(sumFirst(n))
}
}
// ===== Client =====
class Client(serverPath : String) extends Actor {
val server = context.actorSelection(serverPath)
implicit val timeout : Timeout = 5.seconds
override def preStart() : Unit = {
server ! Factorial(5)
server ! SumFirst(10)
}
def receive : Receive = {
case Result(value) =>
println(s"${self.path.name} a reçu : $value")
}
}
// ===== Main =====
object Main extends App {
val system = ActorSystem("MySystem")
val server = system.actorOf(Props[Server], "server")
val client1 = system.actorOf(Props(new Client("/user/server")), "client1")
val client2 = system.actorOf(Props(new Client("/user/server")), "client2")
Thread.sleep(5000)
system.terminate()
}
Sortie attendue (ordre variable) :
Serveur: calcul factorielle de 5
Serveur: somme des 10 premiers
client1 a reçu : 120
client1 a reçu : 55
Serveur: calcul factorielle de 5
Serveur: somme des 10 premiers
client2 a reçu : 120
client2 a reçu : 55
L'ordre exact dépend de l'ordonnanceur — c'est le propre du modèle d'acteurs. Chaque acteur traite ses messages séquentiellement, mais les acteurs entre eux sont concurrents.
9. Le problème des philosophes (Dijkstra)
- 🧠 Penser
- 🍝 Manger
Solution 1 — Superviseur centralisé
Un acteur Superviseur contrôle l'accès aux fourchettes.
Principe :
- Un philosophe demande l'autorisation au superviseur avant de manger
- Le superviseur vérifie si les deux fourchettes sont disponibles
- Si oui ⇒ il accorde l'autorisation
- Sinon ⇒ le philosophe attend
- Après avoir mangé, le philosophe informe le superviseur qu'il libère les fourchettes
Variables d'état du superviseur : tableau[Int] indiquant pour chaque fourchette si elle est libre (true) ou prise (false).
Solution 2 — Distribuée (sans superviseur)
Pas de processus central. Chaque fourchette est un acteur. Un philosophe communique avec ses deux voisines.
Stratégie :
- Un philosophe prend d'abord la fourchette de numéro le plus petit
- Puis la fourchette de numéro le plus grand
- Une fourchette n'est utilisée que par un seul philosophe à la fois
Tous les philosophes prennent les fourchettes dans le même ordre (croissant des numéros). Donc même si plusieurs essaient en même temps, il existe toujours au moins un qui aura les deux : celui qui réussit à prendre la fourchette de plus grand numéro avant les autres.
★ Réviser le chapitre
🃏 Flashcards
val ref = system.actorOf(Props[MaClasse], "nom"). Props décrit comment créer l'acteur, actorOf renvoie un ActorRef.a ! msg ?msg dans la mailbox de a et continue immédiatement. Asynchrone, fire-and-forget. Pas de retour.sender() ?receive, renvoie l'ActorRef de l'expéditeur du message courant. Permet de répondre.ActorRef vs ActorSelection ?ActorRef = référence directe. ActorSelection = chemin résolu à chaque envoi, peut pointer sur 0, 1 ou N acteurs. Préférer ActorRef.receive ?Receive ≡ PartialFunction[Any, Unit]. C'est un pattern matching sur tout type Any qui ne renvoie rien (effet de bord = envoyer des messages).sealed trait + case classes pour les messages ?== sémantique. Sécurise et clarifie le contrat de communication.preStart() ?? (ask) ?Future[Any] qui se résoudra avec la réponse. Nécessite un implicit Timeout. Casse l'asynchronisme pur, à utiliser avec parcimonie.✎ Quiz éclair
a ! msg ?receive, comment répondre à l'expéditeur ?system.actorOf(Props[MyActor], "name") ?Score : 0 / 7 ·
❓ Q/R
Pourquoi un acteur traite-t-il ses messages séquentiellement ?
Note : la concurrence apparaît entre acteurs, pas à l'intérieur. Plusieurs acteurs sur des cœurs différents peuvent fonctionner en parallèle.
Quand utiliser tell (!) vs ask (?) ?
- tell (!) : par défaut. Envoyer un message sans attendre de réponse synchrone. Si une réponse est nécessaire, l'acteur appelant la recevra dans son
receiveviasender(). - ask (?) : quand on a besoin de chaîner une réponse dans un Future (ex : en dehors d'un acteur, dans du code "classique" Scala). Coût : un wrapper Future, un timeout obligatoire, plus de complexité.
tell chaque fois que possible. C'est l'idiomatique Akka.
Comment éviter un deadlock dans le problème des philosophes ?
- Superviseur centralisé : un acteur arbitre l'accès aux fourchettes. Il ne donne jamais une seule fourchette à un philosophe — soit les deux d'un coup, soit aucune.
- Ordre total sur les fourchettes : chaque philosophe prend toujours d'abord la fourchette de plus petit numéro. Ça brise la symétrie du cycle d'attente, garantissant qu'il y a toujours au moins un philosophe qui peut manger.
L'envoi d'un message peut-il échouer silencieusement ?
- Si l'acteur destinataire n'existe plus (terminé), le message va dans la « dead letters queue » — perdu silencieusement.
- Si
receivene matche pas le message reçu (pas decase _), le message est ignoré silencieusement. - Si la mailbox déborde (rare avec la config par défaut), le message peut être abandonné.
case _ => qui log au moins l'erreur, ou de monitor les acteurs critiques.
Quels avantages d'Akka par rapport à une approche thread/lock classique ?
- Pas de mutex à gérer — l'erreur de programmation la plus courante en concurrence disparaît.
- Scalabilité horizontale : on peut déployer les acteurs sur plusieurs machines avec Akka Cluster, sans changer le code.
- Modèle uniforme : local ou distribué, c'est la même API (transparence de localisation).
- Robustesse via la supervision : un acteur enfant qui crashe est redémarré par son parent.
- Léger : un acteur consomme ~300 octets, on peut en avoir des millions sur une seule machine.