Pourquoi les Mutex sont mieux adaptés aux coroutines Kotlin

En Kotlin, on peut faire de la concurrence de deux manières :

  • avec des threads (approche classique JVM),
  • avec des coroutines (approche moderne, plus légère).

Et dès qu’on a un état partagé (cache, compteur, map, “state” d’un service), on doit empêcher plusieurs opérations de le modifier en même temps. C’est là qu’on utilise un verrou : lock, mutex, etc.

La question est : pourquoi, en code basé sur des coroutines, on recommande souvent Mutex plutôt qu’un lock classique (synchronized, ReentrantLock) ?


1) Le point clé : un lock classique bloque un thread

Avec un lock classique, si la ressource est déjà prise :

  • le thread attend,
  • et pendant ce temps il ne fait rien.

C’est comme une personne qui attend devant une porte, sans pouvoir faire autre chose.

Sur la JVM (et sur Android), les threads sont une ressource précieuse :

  • créer et gérer des threads coûte cher,
  • le pool de threads n’est pas infini,
  • si tu bloques trop de threads, tout le système ralentit.

2) Une coroutine n’est pas un thread

Une coroutine est “légère” : tu peux en avoir des milliers, parce qu’elles ne consomment pas une vraie pile (stack) de thread à elles seules.

Une coroutine peut :

  • se suspendre,
  • reprendre plus tard,
  • sans monopoliser un thread.

C’est l’une des forces des coroutines : faire de l’attente sans bloquer.


3) Mutex coroutines : il suspend la coroutine au lieu de bloquer le thread

Le Mutex de kotlinx.coroutines est conçu exactement pour ça :

  • si la ressource est libre ? la coroutine entre,
  • si elle est prise ? la coroutine se suspend,
  • le thread est libéré et peut faire autre chose.

En image :

  • Lock classique : “je reste planté devant la porte”.
  • Mutex coroutines : “je pars faire autre chose, reviens quand c’est mon tour”.

4) Exemple simple : état partagé

Imaginons un cache partagé (map en mémoire). On veut éviter deux écritures en même temps.


import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class MemoryCache {
    private val mutex = Mutex()
    private val map = mutableMapOf<String, String>()

    suspend fun put(key: String, value: String) {
        mutex.withLock {
            map[key] = value
        }
    }

    suspend fun get(key: String): String? =
        mutex.withLock { map[key] }
}

Ici, si deux coroutines appellent put en même temps :

  • une passe,
  • l’autre attend,
  • mais aucun thread n’est bloqué inutilement.

5) Pourquoi “bloquer un thread” est mauvais avec les coroutines

Si tu mélanges coroutines + locks classiques, tu risques :

  • de bloquer des threads du pool (IO, Default),
  • de ralentir d’autres coroutines qui auraient pu tourner,
  • et sur Android, de créer des risques d’ANR si tu bloques le main thread.

Le symptôme typique : “tout marche au début”, puis l’app devient lente quand il y a de la charge.


6) Une règle très pratique

  • Si ton code est thread-based (Executors, Java legacy) ? locks classiques OK.
  • Si ton code est coroutine-first (suspend, async, flows) ? Mutex.

7) Pièges à éviter même avec un Mutex

Mutex = bien adapté, mais il faut rester prudent :

  • Ne pas garder le mutex trop longtemps (sinon tu ralentis tout).
  • Éviter de faire des opérations longues dans le withLock (ex : gros IO, gros calcul).
  • Verrouiller seulement pour la partie “modification d’état”.

Bon pattern :

  • tu prépares les données hors lock,
  • tu lock uniquement pour mettre à jour l’état partagé,
  • tu relâches vite.

Conclusion

Les coroutines sont faites pour “attendre sans bloquer”. Un lock classique bloque un thread, ce qui va contre cette idée.

Un Mutex de coroutines est généralement meilleur parce qu’il :

  • fait attendre une coroutine en la suspendant,
  • ne gaspille pas de threads,
  • s’intègre naturellement avec du code suspend.

Résultat : un code plus scalable, plus fluide sous charge, et plus cohérent avec la philosophie coroutines.