[{"data":1,"prerenderedAt":1976},["ShallowReactive",2],{"blog-post-\u002Fblogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15":3,"related-\u002Fblogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15":1940},{"id":4,"title":5,"alt":6,"authors":7,"body":14,"date":1918,"description":1919,"extension":1920,"image":1921,"meta":1922,"navigation":130,"ogImage":1921,"path":1923,"published":130,"reviewers":1924,"seo":1935,"stem":1936,"tags":1937,"__hash__":1939},"blogs\u002Fblogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Findex.md","Une coroutine n'est pas un thread : Coroutines (1\u002F5)","Couverture de blog HoppR : paysage surréaliste avec ciel vert et champs dorés. Logo HoppR en blanc en haut à droite. Boîte arrondie semi-transparente en bas à gauche contenant le titre \"Une coroutine n'est pas un thread\" en blanc et la ligne \"Kotlin Coroutines 1\u002F5\" en orange.",[8],{"id":9,"name":10,"image":11,"linkedin":12,"x":13},"33bf4462-cd38-80da-845c-c63b2fd024bf","Florian Hirson",".\u002Fassets\u002Fauthor-florian-hirson.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fflorian-hirson\u002F",null,{"type":15,"value":16,"toc":1902},"minimark",[17,24,29,45,57,64,70,197,200,207,210,214,217,236,252,267,288,291,297,304,307,311,318,329,338,344,348,351,362,365,376,379,383,394,397,444,452,458,461,468,479,482,492,499,505,514,609,619,628,635,639,655,735,756,760,770,865,882,885,961,964,970,1151,1157,1160,1240,1250,1254,1266,1345,1356,1362,1369,1373,1392,1395,1468,1477,1480,1508,1551,1554,1558,1708,1728,1732,1761,1764,1767,1771,1898],[18,19,20],"p",{},[21,22,23],"em",{},"Premier épisode d'une série sur les coroutines Kotlin. On part de définitions puis on regardera sous le capot. Fil rouge tout du long : la comparaison avec les threads Java.",[25,26,28],"h2",{"id":27},"le-batch-qui-gardait-des-fantômes-en-mémoire","Le batch qui gardait des fantômes en mémoire",[18,30,31,32,39,40,44],{},"Lors d’une mission précédente, j'hérite d'un job ",[33,34,38],"a",{"href":35,"rel":36},"https:\u002F\u002Fspring.io\u002Fprojects\u002Fspring-batch",[37],"nofollow","Spring Batch"," sur une base de code legacy en Java 8. Son travail : lire un CSV de 2 à 3 Go, supprimer les données existantes en cascade puis réinsérer le tout en base. La demande qui tombe : arrêter le delete en cascade, passer à un ",[41,42,43],"strong",{},"update en delta",". Vu la volumétrie, les données étaient traitées par paquets de 1000 items pour grouper les écritures.",[18,46,47,48,56],{},"Sur le papier, rien de méchant. Sauf qu'en ouvrant le code, je trouve des ",[33,49,52],{"href":50,"rel":51},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F25\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Futil\u002Fconcurrent\u002FFuture.html",[37],[53,54,55],"code",{},"Future"," un peu partout, des variables quasi globales qui trimballent l'état d'un traitement, et zéro documentation. Le job ne tournait jamais en parallèle d'un autre, une seule instance à la fois. Cependant les données des jobs précédents étaient encore présentes en mémoire quand le suivant démarrait, et venaient polluer le delta. Le premier job passait et tous les autres plantaient.",[18,58,59],{},[60,61],"img",{"alt":62,"src":63},"Meme tiré de \"Les Simpsons\" montrant Bart Simpson à l'étage supérieur (inquiet)\net Homer Simpson à l'étage inférieur (criant). Texte : \"This is the worst\nmultithreading bug of my life\" (haut) et \"so far The worst of your life\nthreading multibug\" (bas). Meme humoristique sur les bugs de programmation\nconcurrente.","\u002Fcontent-assets\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Fassets\u002Fimg1.webp",[18,65,66,67,69],{},"Spring Batch n’y était pour rien, la concurrence était bricolée avec des threads lancés manuellement, des ",[53,68,55],{}," dispersés et de l'état mutable partagé sans personne pour dire qui écrit quoi, quand et pourquoi.",[71,72,77],"pre",{"className":73,"code":74,"language":75,"meta":76,"style":76},"language-kotlin shiki shiki-themes github-dark-default","\u002F\u002F Le type de code qui a mal tourné : état partagé mutable + Futures dispersés\nvar cache: MutableList\u003CRow> = mutableListOf()   \u002F\u002F variable quasi globale\n\nfun processChunk(chunk: List\u003CRow>): Future\u003C*> = executor.submit {\n    cache.addAll(transform(chunk))   \u002F\u002F qui écrit ? quand ? dans quel ordre ?\n}\n","kotlin","",[53,78,79,88,125,132,170,191],{"__ignoreMap":76},[80,81,84],"span",{"class":82,"line":83},"line",1,[80,85,87],{"class":86},"sH3jZ","\u002F\u002F Le type de code qui a mal tourné : état partagé mutable + Futures dispersés\n",[80,89,91,95,99,103,106,109,112,115,119,122],{"class":82,"line":90},2,[80,92,94],{"class":93},"suJrU","var",[80,96,98],{"class":97},"sZEs4"," cache: ",[80,100,102],{"class":101},"sQhOw","MutableList",[80,104,105],{"class":97},"\u003C",[80,107,108],{"class":101},"Row",[80,110,111],{"class":97},"> ",[80,113,114],{"class":93},"=",[80,116,118],{"class":117},"sc3cj"," mutableListOf",[80,120,121],{"class":97},"()   ",[80,123,124],{"class":86},"\u002F\u002F variable quasi globale\n",[80,126,128],{"class":82,"line":127},3,[80,129,131],{"emptyLinePlaceholder":130},true,"\n",[80,133,135,138,141,144,147,149,151,154,156,159,161,164,167],{"class":82,"line":134},4,[80,136,137],{"class":93},"fun",[80,139,140],{"class":117}," processChunk",[80,142,143],{"class":97},"(chunk: ",[80,145,146],{"class":101},"List",[80,148,105],{"class":97},[80,150,108],{"class":101},[80,152,153],{"class":97},">): ",[80,155,55],{"class":101},[80,157,158],{"class":97},"\u003C*> ",[80,160,114],{"class":93},[80,162,163],{"class":97}," executor.",[80,165,166],{"class":117},"submit",[80,168,169],{"class":97}," {\n",[80,171,173,176,179,182,185,188],{"class":82,"line":172},5,[80,174,175],{"class":97},"    cache.",[80,177,178],{"class":117},"addAll",[80,180,181],{"class":97},"(",[80,183,184],{"class":117},"transform",[80,186,187],{"class":97},"(chunk))   ",[80,189,190],{"class":86},"\u002F\u002F qui écrit ? quand ? dans quel ordre ?\n",[80,192,194],{"class":82,"line":193},6,[80,195,196],{"class":97},"}\n",[18,198,199],{},"À l'intérieur d'une même exécution, plusieurs threads écrivaient dans ce cache sans la moindre synchronisation. Cette écriture concurrente sur une liste non protégée n'a jamais provoqué de plantage visible, mais elle pouvait perdre des lignes ou corrompre la structure interne de la liste à n'importe quel moment. Deux problèmes distincts se superposaient donc, un état qui débordait de la durée de vie du job et des accès concurrents non gérés sur ce même état.",[18,201,202,203,206],{},"On était en Java 8 : les virtual threads n'existaient pas et n'arriveraient que bien plus tard. Mais même avec les virtual threads, ce bug-là serait resté. Le problème n'était pas le nombre de threads, c'était l'",[41,204,205],{},"absence de structure"," autour de la concurrence.",[18,208,209],{},"C'est le manque que les coroutines viennent combler. Avant de voir comment, il faut se débarrasser d'une idée reçue : non une coroutine n'est pas thread léger.",[25,211,213],{"id":212},"rappel-process-thread-et-coroutine","Rappel : process, thread et coroutine",[18,215,216],{},"Avant d'aller plus loin, il est important de souligner la distinction entre un processus et un thread. Les deux permettent d'exécuter plusieurs actions en même temps, mais pas à la même échelle.",[18,218,219,220,223,224,229,230,235],{},"Un ",[41,221,222],{},"process",", c'est une instance de programme en cours d'exécution avec son environnement isolé. Comme le rappelle la documentation Java, ",[33,225,228],{"href":226,"rel":227},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Ftutorial\u002Fessential\u002Fconcurrency\u002Fprocthread.html",[37],"chaque process a son propre espace mémoire",". Deux process ne peuvent pas partager le même espace mémoire : pour échanger, ils passent par une ",[33,231,234],{"href":232,"rel":233},"https:\u002F\u002Ffr.wikipedia.org\u002Fwiki\u002FCommunication_inter-processus",[37],"communication inter-process (IPC)"," plus lourde et plus lente.",[18,237,238,239,242,243,247,248,251],{},"Le code exécuté sur un ",[41,240,241],{},"thread",", appartient à un process. Ce sont des unités d'exécution qui",[33,244,246],{"href":226,"rel":245},[37]," partagent les ressources du process, comme le même espace d'adressage mémoire",", la ",[53,249,250],{},"heap",", et autres. Parce qu'ils se comportent de manière autonome, on a tendance à les appeler parfois “lightweight processes”. Chaque thread garde tout de même sa propre pile d'exécution, ce fameux ~1 Mo qu'on détaille juste après.",[18,253,254,255,260,261,266],{},"Cette mémoire partagée est à double tranchant. Communiquer entre threads est direct et rapide, mais c'est aussi le terrain le plus favorable aux ",[33,256,259],{"href":257,"rel":258},"https:\u002F\u002Ffr.wikipedia.org\u002Fwiki\u002FSituation_de_comp%C3%A9tition",[37],"race conditions"," et aux ",[33,262,265],{"href":263,"rel":264},"https:\u002F\u002Ffr.wikipedia.org\u002Fwiki\u002FInterblocage",[37],"deadlocks",". Le problème de fond reste la gestion des accès concurrents à une ressource partagée, un problème qui se pose aussi entre deux processus qui échangent par IPC. La mémoire partagée le rend simplement immédiat. On retrouve la moitié du problème de mon batch, de l'état mutable partagé entre threads sans personne pour gérer les accès. L'autre moitié tenait à sa durée de vie.",[18,268,269,270,273,274,277,278,283,284,287],{},"Une autre distinction à clarifier, celle qui revient souvent (logique contre physique) : le thread dont on parle est un ",[41,271,272],{},"thread OS"," (logiciel), celui que l’application crée. A ne pas confondre avec un ",[41,275,276],{},"thread physique",", matériel qui est une vraie unité d'exécution du processeur. Un cœur en offre un, ou ",[33,279,282],{"href":280,"rel":281},"https:\u002F\u002Fwww.intel.com\u002Fcontent\u002Fwww\u002Fus\u002Fen\u002Fgaming\u002Fresources\u002Fhyper-threading.html",[37],"deux avec l'hyper-threading \u002F SMT"," : un CPU 8 cœurs présente ainsi souvent 16 “processeurs logiques” à l'OS. Le CPU n'exécute donc vraiment qu'une poignée de threads OS en parallèle. On peut en créer des milliers côté logiciel, mais c'est le ",[41,285,286],{},"scheduler de l'OS"," qui les fait tourner à tour de rôle sur ces quelques threads physiques, par tranches de temps. Le parallélisme massif qu'on croit avoir est en bonne partie une illusion entretenue par ce scheduler.",[18,289,290],{},"Et les coroutines ? Elles vivent à un niveau d'abstraction plus élevé, tout en s'exécutant à l'intérieur des threads. Un process porte quelques threads ; un thread peut porter des milliers de coroutines multiplexées. D'où la hiérarchie d'imbrication :",[18,292,293],{},[60,294],{"alt":295,"src":296},"Diagramme hiérarchique montrant la relation \"process > threads > coroutines\". Un processus contient la mémoire isolée des autres process, partagée entre ses threads. Trois colonnes représentent trois Thread OS identiques, chacun avec sa propre pile (~1 Mo). Dans chaque thread, plusieurs coroutines (représentées en vert) s'exécutent, avec la mention \"des centaines de milliers\" pour montrer l'échelle.","\u002Fcontent-assets\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Fassets\u002Fimg2.webp",[18,298,299,300,303],{},"L'échelle à retenir : ",[41,301,302],{},"process > threads > coroutines",". Chaque étage descend d'un cran en utilisation mémoire et monte d'un cran en nombre. Les process sont isolés (l'un tombe, les autres tiennent) ; les threads d'un même process partagent son sort, puisqu'ils partagent sa mémoire. C'est cette propriété qui explique à la fois la puissance et les risques de l’utilisation des threads, et c'est le contexte dans lequel les coroutines vont mettre de l'ordre.",[18,305,306],{},"Concrètement, une coroutine est une fonction qui sait s'interrompre en cours de route puis reprendre plus tard, exactement là où elle s'était arrêtée. Un thread réserve une pile complète pour tenir cet état. Une coroutine garde seulement un petit objet sur le tas, avec son point de reprise et ses variables locales. C'est ce qui permet d'en empiler des milliers sur un seul thread. Le reste de l'article détaille les conséquences de cette différence.",[25,308,310],{"id":309},"pourquoi-les-threads-coûtent-cher","Pourquoi les threads coûtent cher",[18,312,313,314,317],{},"Un thread de la JVM c'est un thread du système d'exploitation : il est solide, mais lourd. Chacun réserve sa propre pile mémoire, en général autour de 1 Mo par défaut sur une JVM 64 bits (réglable via ",[53,315,316],{},"-Xss","). Et c'est l'OS qui les gère.",[18,319,320,321,324,325,328],{},"Concrètement ça veut dire deux choses. D'abord, on ne peut pas en créer des centaines de milliers : la mémoire explose avant. Ensuite, un thread ",[41,322,323],{},"bloqué"," (sur un traitement synchrone lourd, un ",[53,326,327],{},"Thread.sleep",", une requête réseau, un accès disque) reste un thread mobilisé qui ne fait rien, tout en occupant sa mémoire.",[18,330,331,332,337],{},"Il y a un troisième coût, moins visible : le changement de contexte. Quand l'OS bascule d'un thread à l'autre, il passe en mode noyau pour sauvegarder les registres du thread sortant et recharger ceux du suivant. Au passage, les caches CPU et le ",[33,333,336],{"href":334,"rel":335},"https:\u002F\u002Ffr.wikipedia.org\u002Fwiki\u002FTranslation_lookaside_buffer",[37],"TLB"," se retrouvent remplis de données qui ne servent plus. Chaque bascule coûte de l'ordre de la microseconde. Sur un pool surdimensionné, le CPU finit par passer plus de temps à jongler entre les threads qu'à faire avancer le travail.",[18,339,340],{},[60,341],{"alt":342,"src":343},"Diagramme comparatif \"Thread OS vs Coroutine : ce que porte une unité de travail\". Colonne gauche (bleue) : Thread OS géré par le scheduler du noyau, contenant pile d'exécution (~1 Mo réservé par thread), structures noyau (descripteur de tâche), TLS\u002Ferrno\u002Fmasque de signaux, entrée dans la table du scheduler. Empreinte ~1 Mo, changement de contexte ~1 µs en mode noyau. Colonne droite (verte) : Coroutine gérée par le dispatcher dans la JVM, contenant objet Continuation (reprend l'exécution) et variables locales capturées. Pas de réserve de pile, l'OS ignore son existence. Empreinte quelques centaines d'octets, changement de contexte ~10 ns par retour de fonction. Note en bas : schéma non proportionnel, rapport réel d'empreinte de l'ordre de 1 pour 10 000.","\u002Fcontent-assets\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Fassets\u002Fimg3.webp",[25,345,347],{"id":346},"concurrence-et-parallélisme","Concurrence et parallélisme",[18,349,350],{},"Deuxième point à aborder : la confusion entre concurrence et parallélisme.",[18,352,353,354,357,358,361],{},"La ",[41,355,356],{},"concurrence",", c'est gérer plusieurs tâches en cours au même moment, sans forcément les faire avancer au même instant. Le ",[41,359,360],{},"parallélisme",", c'est exécuter plusieurs tâches littéralement en même temps, sur des unités de calcul distinctes. Rob Pike résume la nuance en une formule connue : la concurrence consiste à s'occuper de plusieurs choses à la fois, le parallélisme à en exécuter plusieurs à la fois.",[18,363,364],{},"Le parallélisme dépend du matériel. Il vous faut plusieurs cœurs, sinon il n'existe pas. La concurrence est un modèle d'organisation du code qui tient même sur un seul cœur.",[18,366,367,368,371,372,375],{},"Une coroutine est concurrente par nature. Lancer mille coroutines avec ",[53,369,370],{},"launch"," ne les fait pas tourner en même temps, ça déclare mille tâches qui vont s'entrelacer sur les threads disponibles. Le parallélisme arrive seulement si le dispatcher les répartit sur plusieurs threads, ce que fait ",[53,373,374],{},"Dispatchers.Default"," avec un pool dimensionné sur le nombre de cœurs. Sur un dispatcher mono-thread, vous avez de la concurrence sans une once de parallélisme.",[18,377,378],{},"C'est exactement l'illusion que le scheduler de l'OS entretient déjà avec les threads, reproduite un étage plus haut par le dispatcher.",[25,380,382],{"id":381},"suspendre-puis-reprendre","Suspendre, puis reprendre",[18,384,385,386,389,390,393],{},"Une coroutine part d'une autre idée. Plutôt que de bloquer un thread en attendant, elle sait se ",[41,387,388],{},"suspendre"," à un point précis, libérer le thread pour qu'il fasse autre chose, puis ",[41,391,392],{},"reprendre"," plus tard là où elle s'était arrêtée.",[18,395,396],{},"La différence tient dans deux fonctions qui se ressemblent mais n'ont rien à voir :",[71,398,400],{"className":73,"code":399,"language":75,"meta":76,"style":76},"\u002F\u002F Bloque le thread pendant 1 seconde : il ne peut rien faire d'autre\nThread.sleep(1000)\n\n\u002F\u002F Suspend la coroutine pendant 1 seconde : le thread est libre entre-temps\ndelay(1000)\n",[53,401,402,407,424,428,433],{"__ignoreMap":76},[80,403,404],{"class":82,"line":83},[80,405,406],{"class":86},"\u002F\u002F Bloque le thread pendant 1 seconde : il ne peut rien faire d'autre\n",[80,408,409,412,415,417,421],{"class":82,"line":90},[80,410,411],{"class":97},"Thread.",[80,413,414],{"class":117},"sleep",[80,416,181],{"class":97},[80,418,420],{"class":419},"sFSAA","1000",[80,422,423],{"class":97},")\n",[80,425,426],{"class":82,"line":127},[80,427,131],{"emptyLinePlaceholder":130},[80,429,430],{"class":82,"line":134},[80,431,432],{"class":86},"\u002F\u002F Suspend la coroutine pendant 1 seconde : le thread est libre entre-temps\n",[80,434,435,438,440,442],{"class":82,"line":172},[80,436,437],{"class":117},"delay",[80,439,181],{"class":97},[80,441,420],{"class":419},[80,443,423],{"class":97},[18,445,446,448,449,451],{},[53,447,327],{}," immobilise un thread OS. ",[53,450,437],{}," ne bloque personne : il note « reviens dans une seconde » et rend la main. Pendant cette seconde, le même thread peut faire avancer des milliers d'autres coroutines.",[18,453,454],{},[60,455],{"alt":456,"src":457},"Diagramme \"Bloquer des threads vs suspendre des coroutines\". Partie haute (bleue) : Thread.sleep avec deux tâches sur deux threads distincts. Thread 1 exécute print(\"start 1\"), puis sleep(1000) le mobilise, avant print(\"fin 1\"). Thread 2 exécute print(\"start 2\"), sleep(600) le mobilise, avant print(\"fin 2\"). Deux piles réservées pour deux attentes ; aucun thread ne peut servir à autre chose entre-temps. Partie basse (verte) : delay avec les mêmes deux tâches sur un seul thread qui ne s'arrête jamais. Timeline montrant launch{1} et launch{2}, puis Coroutine 1 suspendue via delay(1000), Coroutine 2 suspendue via delay(600), suspension libérant le thread (qui fait avancer d'autres coroutines), puis reprise de fin 2 et fin 1. Une seule pile pour les deux attentes. Coroutine 2 reprend avant Coroutine 1, chacune à la fin de son propre delay.","\u002Fcontent-assets\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Fassets\u002Fimg4.webp",[18,459,460],{},"Cette différence de comportement vient du modèle d'ordonnancement.",[18,462,463,464,467],{},"L'OS ",[41,465,466],{},"préempte"," : il interrompt un thread quand il le décide, par tranches de temps, sans lui demander son avis. Le code n'a aucun contrôle sur le moment de la bascule, ni même conscience qu'elle a eu lieu. C'est robuste, puisqu'un thread parti en boucle infinie n'empêche pas les autres d'avancer.",[18,469,470,471,474,475,478],{},"Une coroutine ",[41,472,473],{},"coopère",". Elle garde son thread tant qu'elle a du travail à faire, et elle rend la main uniquement à un point de suspension, donc à l'appel d'une fonction ",[53,476,477],{},"suspend",". Entre deux points de suspension, personne ne l'interrompt.",[18,480,481],{},"Ce modèle explique pourquoi les coroutines sont taillées pour l'i\u002Fo. Un appel réseau ou une lecture disque passe l'essentiel de son temps à attendre une réponse, sans consommer de CPU. La coroutine se suspend pendant cette attente et rend son thread aux autres. Vous récupérez le temps mort au lieu de le payer en piles immobilisées.",[18,483,484,485,487,488,491],{},"L'envers du décor est tout aussi utile à connaître. Un calcul long sans point de suspension ne rend jamais la main, monopolise son thread et bloque les autres coroutines du même dispatcher. Pour ce genre de travail, on bascule explicitement sur ",[53,486,374],{},", ou on insère un ",[53,489,490],{},"yield()"," pour créer un point de coopération. On y revient avec les dispatchers dans l'épisode 2.",[18,493,494,495,498],{},"C'est ça, le modèle mental à comprendre : une coroutine est une ",[41,496,497],{},"unité de travail suspendable",", pas un thread. Plusieurs milliers de coroutines peuvent tourner sur une poignée de threads.",[25,500,502,504],{"id":501},"suspend-vu-de-lextérieur",[53,503,477],{}," vu de l'extérieur",[18,506,507,508,510,511,513],{},"Le mot-clé qui rend tout ça possible, c'est ",[53,509,477],{},". Une fonction marquée ",[53,512,477],{}," est une fonction qui peut se mettre en pause sans bloquer son thread.",[71,515,517],{"className":73,"code":516,"language":75,"meta":76,"style":76},"suspend fun loadUser(id: Long): User {\n    \nval profile = api.getProfile(id)    \n\u002F\u002F appel réseau, potentiellement suspendu\n    val rights = api.getRights(id)      \u002F\u002F idem\n    return User(profile, rights)\n}\n",[53,518,519,543,548,567,572,593,604],{"__ignoreMap":76},[80,520,521,523,526,529,532,535,538,541],{"class":82,"line":83},[80,522,477],{"class":93},[80,524,525],{"class":93}," fun",[80,527,528],{"class":117}," loadUser",[80,530,531],{"class":97},"(id: ",[80,533,534],{"class":101},"Long",[80,536,537],{"class":97},"): ",[80,539,540],{"class":101},"User",[80,542,169],{"class":97},[80,544,545],{"class":82,"line":90},[80,546,547],{"class":97},"    \n",[80,549,550,553,556,558,561,564],{"class":82,"line":127},[80,551,552],{"class":93},"val",[80,554,555],{"class":97}," profile ",[80,557,114],{"class":93},[80,559,560],{"class":97}," api.",[80,562,563],{"class":117},"getProfile",[80,565,566],{"class":97},"(id)    \n",[80,568,569],{"class":82,"line":134},[80,570,571],{"class":86},"\u002F\u002F appel réseau, potentiellement suspendu\n",[80,573,574,577,580,582,584,587,590],{"class":82,"line":172},[80,575,576],{"class":93},"    val",[80,578,579],{"class":97}," rights ",[80,581,114],{"class":93},[80,583,560],{"class":97},[80,585,586],{"class":117},"getRights",[80,588,589],{"class":97},"(id)      ",[80,591,592],{"class":86},"\u002F\u002F idem\n",[80,594,595,598,601],{"class":82,"line":193},[80,596,597],{"class":93},"    return",[80,599,600],{"class":117}," User",[80,602,603],{"class":97},"(profile, rights)\n",[80,605,607],{"class":82,"line":606},7,[80,608,196],{"class":97},[18,610,611,612,615,616,618],{},"Ce code se lit comme du code séquentiel classique, de haut en bas. Pas de callback, pas de ",[53,613,614],{},".then()",", pas de ",[53,617,55],{}," à composer à la main. Pourtant, aux points d'appel réseau, la fonction peut se suspendre et libérer le thread.",[18,620,621,622,624,625,627],{},"Une seule règle à retenir pour l'instant : une fonction ",[53,623,477],{}," ne s'appelle que depuis une autre fonction ",[53,626,477],{}," ou depuis un coroutine builder.",[18,629,630,631,634],{},"Cette règle a un effet de bord pratique. La capacité à se suspendre remonte dans les signatures, et reste donc lisible dans le code. À l'appel, rien ne distingue visuellement ",[53,632,633],{},"api.getProfile(id)"," d'un appel classique, mais IntelliJ marque chaque point de suspension d'une icône dans la gouttière, et la définition de la fonction appelée porte le mot-clé.",[25,636,638],{"id":637},"premiers-pas-concrets","Premiers pas concrets",[18,640,641,642,647,648,650,651,654],{},"Pour lancer une coroutine, il faut un ",[33,643,646],{"href":644,"rel":645},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fcoroutines-basics.html#coroutine-builder-functions",[37],"builder",". Le plus simple pour démarrer, c'est ",[53,649,370],{}," à l'intérieur d'un ",[53,652,653],{},"runBlocking"," :",[71,656,658],{"className":73,"code":657,"language":75,"meta":76,"style":76},"fun main() = runBlocking {\n    launch {\n        delay(1000)\n        println(\"World\")\n    }\n    println(\"Hello\")\n}\n\u002F\u002F Affiche \"Hello\" tout de suite, puis \"World\" une seconde plus tard\n",[53,659,660,677,684,695,708,713,725,729],{"__ignoreMap":76},[80,661,662,664,667,670,672,675],{"class":82,"line":83},[80,663,137],{"class":93},[80,665,666],{"class":117}," main",[80,668,669],{"class":97},"() ",[80,671,114],{"class":93},[80,673,674],{"class":117}," runBlocking",[80,676,169],{"class":97},[80,678,679,682],{"class":82,"line":90},[80,680,681],{"class":117},"    launch",[80,683,169],{"class":97},[80,685,686,689,691,693],{"class":82,"line":127},[80,687,688],{"class":117},"        delay",[80,690,181],{"class":97},[80,692,420],{"class":419},[80,694,423],{"class":97},[80,696,697,700,702,706],{"class":82,"line":134},[80,698,699],{"class":117},"        println",[80,701,181],{"class":97},[80,703,705],{"class":704},"s9uIt","\"World\"",[80,707,423],{"class":97},[80,709,710],{"class":82,"line":172},[80,711,712],{"class":97},"    }\n",[80,714,715,718,720,723],{"class":82,"line":193},[80,716,717],{"class":117},"    println",[80,719,181],{"class":97},[80,721,722],{"class":704},"\"Hello\"",[80,724,423],{"class":97},[80,726,727],{"class":82,"line":606},[80,728,196],{"class":97},[80,730,732],{"class":82,"line":731},8,[80,733,734],{"class":86},"\u002F\u002F Affiche \"Hello\" tout de suite, puis \"World\" une seconde plus tard\n",[18,736,737,739,740,743,744,746,747,749,750,749,753,755],{},[53,738,370],{}," démarre une coroutine et n'attend pas qu'elle finisse (fire-and-forget). Le ",[53,741,742],{},"println(\"Hello\")"," s'exécute donc avant le ",[53,745,705],{}," suspendu. On reviendra en détail sur les builders (",[53,748,370],{},", ",[53,751,752],{},"async",[53,754,653],{},") dans l'épisode 2.",[25,757,759],{"id":758},"le-poids-de-la-syntaxe-java-vs-kotlin","Le poids de la syntaxe : Java vs Kotlin",[18,761,762,763,766,767,654],{},"Reprenons le ",[53,764,765],{},"loadUser"," d'avant. En Java, pour enchaîner ces appels sans bloquer le thread, on sort ",[53,768,769],{},"CompletableFuture",[71,771,775],{"className":772,"code":773,"language":774,"meta":76,"style":76},"language-java shiki shiki-themes github-dark-default","\u002F\u002F Java : non-bloquant, mais la logique se dilue dans les callbacks\nCompletableFuture\u003CUser> loadUser(long id) {\n    return api.getProfileAsync(id)\n        .thenCompose(profile ->\n            api.getRightsAsync(id)\n                .thenApply(rights -> new User(profile, rights)));\n}\n","java",[53,776,777,782,803,815,829,839,861],{"__ignoreMap":76},[80,778,779],{"class":82,"line":83},[80,780,781],{"class":86},"\u002F\u002F Java : non-bloquant, mais la logique se dilue dans les callbacks\n",[80,783,784,786,788,790,793,795,797,800],{"class":82,"line":90},[80,785,769],{"class":97},[80,787,105],{"class":93},[80,789,540],{"class":97},[80,791,792],{"class":93},">",[80,794,528],{"class":117},[80,796,181],{"class":97},[80,798,799],{"class":93},"long",[80,801,802],{"class":97}," id) {\n",[80,804,805,807,809,812],{"class":82,"line":127},[80,806,597],{"class":93},[80,808,560],{"class":97},[80,810,811],{"class":117},"getProfileAsync",[80,813,814],{"class":97},"(id)\n",[80,816,817,820,823,826],{"class":82,"line":134},[80,818,819],{"class":97},"        .",[80,821,822],{"class":117},"thenCompose",[80,824,825],{"class":97},"(profile ",[80,827,828],{"class":93},"->\n",[80,830,831,834,837],{"class":82,"line":172},[80,832,833],{"class":97},"            api.",[80,835,836],{"class":117},"getRightsAsync",[80,838,814],{"class":97},[80,840,841,844,847,850,853,856,858],{"class":82,"line":193},[80,842,843],{"class":97},"                .",[80,845,846],{"class":117},"thenApply",[80,848,849],{"class":97},"(rights ",[80,851,852],{"class":93},"->",[80,854,855],{"class":93}," new",[80,857,600],{"class":117},[80,859,860],{"class":97},"(profile, rights)));\n",[80,862,863],{"class":82,"line":606},[80,864,196],{"class":97},[18,866,867,868,871,872,874,875,877,878,881],{},"Ça fonctionne, mais la logique métier (construire un ",[53,869,870],{},"Utilisateur"," à partir d'un profil et de droits) se retrouve noyée dans les ",[53,873,822],{}," \u002F ",[53,876,846],{},". Ajoutez la gestion d'erreur (",[53,879,880],{},"exceptionally","), un timeout, une troisième dépendance, et l'escalier de callbacks s'allonge vite.",[18,883,884],{},"La version Kotlin reste plate et séquentielle :",[71,886,888],{"className":73,"code":887,"language":75,"meta":76,"style":76},"\u002F\u002F Kotlin : non-bloquant aussi, mais ça se lit comme du code normal\nsuspend fun loadUser(id: Long): User {\n\n    val profile = api.getProfile(id)\n    val rights = api.getRights(id)\n\n    return User(profile, rights)\n}\n",[53,889,890,895,913,917,931,945,949,957],{"__ignoreMap":76},[80,891,892],{"class":82,"line":83},[80,893,894],{"class":86},"\u002F\u002F Kotlin : non-bloquant aussi, mais ça se lit comme du code normal\n",[80,896,897,899,901,903,905,907,909,911],{"class":82,"line":90},[80,898,477],{"class":93},[80,900,525],{"class":93},[80,902,528],{"class":117},[80,904,531],{"class":97},[80,906,534],{"class":101},[80,908,537],{"class":97},[80,910,540],{"class":101},[80,912,169],{"class":97},[80,914,915],{"class":82,"line":127},[80,916,131],{"emptyLinePlaceholder":130},[80,918,919,921,923,925,927,929],{"class":82,"line":134},[80,920,576],{"class":93},[80,922,555],{"class":97},[80,924,114],{"class":93},[80,926,560],{"class":97},[80,928,563],{"class":117},[80,930,814],{"class":97},[80,932,933,935,937,939,941,943],{"class":82,"line":172},[80,934,576],{"class":93},[80,936,579],{"class":97},[80,938,114],{"class":93},[80,940,560],{"class":97},[80,942,586],{"class":117},[80,944,814],{"class":97},[80,946,947],{"class":82,"line":193},[80,948,131],{"emptyLinePlaceholder":130},[80,950,951,953,955],{"class":82,"line":606},[80,952,597],{"class":93},[80,954,600],{"class":117},[80,956,603],{"class":97},[80,958,959],{"class":82,"line":731},[80,960,196],{"class":97},[18,962,963],{},"Même comportement non-bloquant, mais le compilateur s'occupe de la tuyauterie (on verra exactement laquelle dans l'épisode 5). C'est ce qu'on résume souvent par “écrire de l'asynchrone comme du synchrone”.",[18,965,966,967,654],{},"Et pour lancer plusieurs tâches en parallèle ? En Java, on passe par un ",[53,968,969],{},"ExecutorService",[71,971,973],{"className":772,"code":972,"language":774,"meta":76,"style":76},"\u002F\u002F Java : soumettre, garder les Future, join à la main, fermer l'executor\nExecutorService executor = Executors.newFixedThreadPool(8);\nList\u003CFuture\u003COutcome>> futures = new ArrayList\u003C>();\nfor (Task t : tasks) {\n    futures.add(executor.submit(() -> process(t)));\n}\nList\u003COutcome> results = new ArrayList\u003C>();\nfor (Future\u003COutcome> f : futures) {\n    results.add(f.get());   \u002F\u002F bloque, et peut lever ExecutionException\n}\nexecutor.shutdown();\n",[53,974,975,980,1004,1030,1046,1070,1074,1093,1114,1134,1139],{"__ignoreMap":76},[80,976,977],{"class":82,"line":83},[80,978,979],{"class":86},"\u002F\u002F Java : soumettre, garder les Future, join à la main, fermer l'executor\n",[80,981,982,984,987,990,993,996,998,1001],{"class":82,"line":90},[80,983,969],{"class":97},[80,985,986],{"class":97}," executor",[80,988,989],{"class":93}," =",[80,991,992],{"class":97}," Executors.",[80,994,995],{"class":117},"newFixedThreadPool",[80,997,181],{"class":97},[80,999,1000],{"class":419},"8",[80,1002,1003],{"class":97},");\n",[80,1005,1006,1008,1010,1012,1014,1017,1020,1023,1025,1027],{"class":82,"line":127},[80,1007,146],{"class":97},[80,1009,105],{"class":101},[80,1011,55],{"class":97},[80,1013,105],{"class":101},[80,1015,1016],{"class":93},"Outcome",[80,1018,1019],{"class":101},">> ",[80,1021,1022],{"class":97},"futures",[80,1024,989],{"class":93},[80,1026,855],{"class":93},[80,1028,1029],{"class":97}," ArrayList\u003C>();\n",[80,1031,1032,1035,1038,1041,1043],{"class":82,"line":134},[80,1033,1034],{"class":93},"for",[80,1036,1037],{"class":97}," (Task",[80,1039,1040],{"class":97}," t",[80,1042,654],{"class":93},[80,1044,1045],{"class":97}," tasks) {\n",[80,1047,1048,1051,1054,1057,1059,1062,1064,1067],{"class":82,"line":172},[80,1049,1050],{"class":97},"    futures.",[80,1052,1053],{"class":117},"add",[80,1055,1056],{"class":97},"(executor.",[80,1058,166],{"class":117},[80,1060,1061],{"class":97},"(() ",[80,1063,852],{"class":93},[80,1065,1066],{"class":117}," process",[80,1068,1069],{"class":97},"(t)));\n",[80,1071,1072],{"class":82,"line":193},[80,1073,196],{"class":97},[80,1075,1076,1078,1080,1082,1084,1087,1089,1091],{"class":82,"line":606},[80,1077,146],{"class":97},[80,1079,105],{"class":101},[80,1081,1016],{"class":93},[80,1083,111],{"class":101},[80,1085,1086],{"class":97},"results",[80,1088,989],{"class":93},[80,1090,855],{"class":93},[80,1092,1029],{"class":97},[80,1094,1095,1097,1100,1102,1104,1106,1109,1111],{"class":82,"line":731},[80,1096,1034],{"class":93},[80,1098,1099],{"class":97}," (Future",[80,1101,105],{"class":101},[80,1103,1016],{"class":93},[80,1105,111],{"class":101},[80,1107,1108],{"class":97},"f",[80,1110,654],{"class":93},[80,1112,1113],{"class":97}," futures) {\n",[80,1115,1117,1120,1122,1125,1128,1131],{"class":82,"line":1116},9,[80,1118,1119],{"class":97},"    results.",[80,1121,1053],{"class":117},[80,1123,1124],{"class":97},"(f.",[80,1126,1127],{"class":117},"get",[80,1129,1130],{"class":97},"());   ",[80,1132,1133],{"class":86},"\u002F\u002F bloque, et peut lever ExecutionException\n",[80,1135,1137],{"class":82,"line":1136},10,[80,1138,196],{"class":97},[80,1140,1142,1145,1148],{"class":82,"line":1141},11,[80,1143,1144],{"class":97},"executor.",[80,1146,1147],{"class":117},"shutdown",[80,1149,1150],{"class":97},"();\n",[18,1152,1153,1154,1156],{},"C'est très exactement le style qui traînait dans mon batch : des ",[53,1155,55],{}," qu'on collecte, qu'on join, un executor qu'il faut penser à fermer. Et surtout, rien ne relie ces tâches entre elles. Si l'une échoue, les autres continuent dans leur coin, et l'état à moitié écrit reste en mémoire.",[18,1158,1159],{},"En Kotlin :",[71,1161,1163],{"className":73,"code":1162,"language":75,"meta":76,"style":76},"\u002F\u002F Kotlin : les enfants sont liés au scope, attendus et annulés ensemble\nsuspend fun processAll(tasks: List\u003CTask>): List\u003COutcome> = coroutineScope {\n    tasks.map { t -> async { process(t) } }.awaitAll()\n}\n",[53,1164,1165,1170,1206,1236],{"__ignoreMap":76},[80,1166,1167],{"class":82,"line":83},[80,1168,1169],{"class":86},"\u002F\u002F Kotlin : les enfants sont liés au scope, attendus et annulés ensemble\n",[80,1171,1172,1174,1176,1179,1182,1184,1186,1189,1191,1193,1195,1197,1199,1201,1204],{"class":82,"line":90},[80,1173,477],{"class":93},[80,1175,525],{"class":93},[80,1177,1178],{"class":117}," processAll",[80,1180,1181],{"class":97},"(tasks: ",[80,1183,146],{"class":101},[80,1185,105],{"class":97},[80,1187,1188],{"class":101},"Task",[80,1190,153],{"class":97},[80,1192,146],{"class":101},[80,1194,105],{"class":97},[80,1196,1016],{"class":101},[80,1198,111],{"class":97},[80,1200,114],{"class":93},[80,1202,1203],{"class":117}," coroutineScope",[80,1205,169],{"class":97},[80,1207,1208,1211,1214,1217,1219,1222,1225,1227,1230,1233],{"class":82,"line":127},[80,1209,1210],{"class":97},"    tasks.",[80,1212,1213],{"class":117},"map",[80,1215,1216],{"class":97}," { t ",[80,1218,852],{"class":93},[80,1220,1221],{"class":117}," async",[80,1223,1224],{"class":97}," { ",[80,1226,222],{"class":117},[80,1228,1229],{"class":97},"(t) } }.",[80,1231,1232],{"class":117},"awaitAll",[80,1234,1235],{"class":97},"()\n",[80,1237,1238],{"class":82,"line":134},[80,1239,196],{"class":97},[18,1241,1242,1243,1245,1246,1249],{},"Pas d'executor à fermer, pas de liste de ",[53,1244,55],{}," à gérer à la main. Le ",[53,1247,1248],{},"coroutineScope"," attend tous ses enfants, et si l'un plante, il annule les autres au lieu de les laisser tourner. C'est un premier aperçu de la structured concurrency, le sujet de l'épisode 2.",[25,1251,1253],{"id":1252},"lexemple-qui-frappe","L'exemple qui frappe",[18,1255,353,1256,1261,1262,1265],{},[33,1257,1260],{"href":1258,"rel":1259},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fcoroutines-basics.html",[37],"documentation officielle de Kotlin"," propose une démonstration qui vaut tous les discours. On lance ",[41,1263,1264],{},"50 000 coroutines",", chacune attend cinq secondes puis affiche un point :",[71,1267,1269],{"className":73,"code":1268,"language":75,"meta":76,"style":76},"suspend fun main() = coroutineScope {\n    repeat(50_000) {\n        launch {\n            delay(5.seconds)\n            print(\".\")\n        }\n    }\n}\n",[53,1270,1271,1287,1300,1307,1320,1332,1337,1341],{"__ignoreMap":76},[80,1272,1273,1275,1277,1279,1281,1283,1285],{"class":82,"line":83},[80,1274,477],{"class":93},[80,1276,525],{"class":93},[80,1278,666],{"class":117},[80,1280,669],{"class":97},[80,1282,114],{"class":93},[80,1284,1203],{"class":117},[80,1286,169],{"class":97},[80,1288,1289,1292,1294,1297],{"class":82,"line":90},[80,1290,1291],{"class":117},"    repeat",[80,1293,181],{"class":97},[80,1295,1296],{"class":419},"50_000",[80,1298,1299],{"class":97},") {\n",[80,1301,1302,1305],{"class":82,"line":127},[80,1303,1304],{"class":117},"        launch",[80,1306,169],{"class":97},[80,1308,1309,1312,1314,1317],{"class":82,"line":134},[80,1310,1311],{"class":117},"            delay",[80,1313,181],{"class":97},[80,1315,1316],{"class":419},"5",[80,1318,1319],{"class":97},".seconds)\n",[80,1321,1322,1325,1327,1330],{"class":82,"line":172},[80,1323,1324],{"class":117},"            print",[80,1326,181],{"class":97},[80,1328,1329],{"class":704},"\".\"",[80,1331,423],{"class":97},[80,1333,1334],{"class":82,"line":193},[80,1335,1336],{"class":97},"        }\n",[80,1338,1339],{"class":82,"line":606},[80,1340,712],{"class":97},[80,1342,1343],{"class":82,"line":731},[80,1344,196],{"class":97},[18,1346,1347,1348,1351,1352,1355],{},"Ça tourne sans problème. La même chose avec 50 000 threads (",[53,1349,1350],{},"repeat(50_000) { thread { Thread.sleep(5000) ... } }",") part en ",[53,1353,1354],{},"OutOfMemoryError",", ou ralentit brutalement la création des threads.",[18,1357,1358],{},[60,1359],{"alt":1360,"src":1361},"Scène de Star Wars dans une navette spatiale futuriste. Obi-Wan Kenobi au\ncentre portant l'uniforme Jedi (tunique blanche et robe marron), encadré par\ndeux figures de côté. Baies vitrées montrant une vue de l'espace et des\nstructures en arrière-plan. Texte en jaune : \"200,000 units are ready, with a\nmillion more well on the way\". Meme basé sur une scène célèbre du film.","\u002Fcontent-assets\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Fassets\u002Fimg5.webp",[18,1363,1364,1365,1368],{},"Les ordres de grandeur donnés par la doc Kotlin sont parlants : pour 50 000 unités de travail, on parle d'environ ",[41,1366,1367],{},"500 Mo côté coroutines contre jusqu'à 100 Go côté threads",", puisque chaque thread réclame sa propre pile mémoire. C'est le même écart que dans mon batch, à ceci près qu'ici il se voit tout de suite. Cet écart se paie tous les jours sur un backend qui sert plusieurs milliers d'utilisateurs en même temps, où chaque requête attend une base ou des services tiers. Un thread par requête sature la thread pool alors que le CPU tourne à 15%, quand une coroutine qui attend rend simplement son thread aux autres.",[25,1370,1372],{"id":1371},"côté-java-et-les-virtual-threads","Côté Java : et les virtual threads ?",[18,1374,1375,1376,1379,1380,1385,1386,1391],{},"La question se pose forcément : Java a fini par apporter les ",[41,1377,1378],{},"virtual threads",", finalisés dans le ",[33,1381,1384],{"href":1382,"rel":1383},"https:\u002F\u002Fwww.infoq.com\u002Fnews\u002F2023\u002F09\u002Fjava21-released",[37],"JDK 21"," sorti le 19 septembre 2023 (",[33,1387,1390],{"href":1388,"rel":1389},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F444",[37],"JEP 444",", après deux previews en JDK 19 et 20). Un virtual thread est justement un thread léger, stocké sur le tas plutôt qu'appuyé en permanence sur un thread OS. On peut en créer des centaines de milliers. Sur le pur “les threads coûtent trop cher”, la JVM a donc comblé son retard.",[18,1393,1394],{},"Côté syntaxe, on reste dans le monde des threads, en un peu plus léger :",[71,1396,1398],{"className":772,"code":1397,"language":774,"meta":76,"style":76},"\u002F\u002F Java 21 : un virtual thread par tâche. La mémoire n'est plus un souci...\ntry (var executor = Executors.newVirtualThreadPerTaskExecutor()) {\n    for (Task t : tasks) {\n        executor.submit(() -> process(t));\n    }\n} \u002F\u002F le try-with-resources attend la fin de toutes les tâches\n",[53,1399,1400,1405,1427,1440,1456,1460],{"__ignoreMap":76},[80,1401,1402],{"class":82,"line":83},[80,1403,1404],{"class":86},"\u002F\u002F Java 21 : un virtual thread par tâche. La mémoire n'est plus un souci...\n",[80,1406,1407,1410,1413,1415,1417,1419,1421,1424],{"class":82,"line":90},[80,1408,1409],{"class":93},"try",[80,1411,1412],{"class":97}," (",[80,1414,94],{"class":93},[80,1416,986],{"class":97},[80,1418,989],{"class":93},[80,1420,992],{"class":97},[80,1422,1423],{"class":117},"newVirtualThreadPerTaskExecutor",[80,1425,1426],{"class":97},"()) {\n",[80,1428,1429,1432,1434,1436,1438],{"class":82,"line":127},[80,1430,1431],{"class":93},"    for",[80,1433,1037],{"class":97},[80,1435,1040],{"class":97},[80,1437,654],{"class":93},[80,1439,1045],{"class":97},[80,1441,1442,1445,1447,1449,1451,1453],{"class":82,"line":134},[80,1443,1444],{"class":97},"        executor.",[80,1446,166],{"class":117},[80,1448,1061],{"class":97},[80,1450,852],{"class":93},[80,1452,1066],{"class":117},[80,1454,1455],{"class":97},"(t));\n",[80,1457,1458],{"class":82,"line":172},[80,1459,712],{"class":97},[80,1461,1462,1465],{"class":82,"line":193},[80,1463,1464],{"class":97},"} ",[80,1466,1467],{"class":86},"\u002F\u002F le try-with-resources attend la fin de toutes les tâches\n",[18,1469,1470,1471,1473,1474,1476],{},"C'est plus léger que l'",[53,1472,969],{}," classique, mais le modèle n'a pas changé : on soumet des tâches à un executor, on récupère des ",[53,1475,55],{},", on raisonne en threads. C'est confortable pour migrer du code bloquant existant, moins pour composer des traitements les uns avec les autres.",[18,1478,1479],{},"Est ce que les coroutines sont devenues inutiles ? Non et pour deux raisons :",[1481,1482,1483,1493],"ul",{},[1484,1485,1486,1487,1492],"li",{},"Les coroutines de Kotlin sont stables depuis la ",[33,1488,1491],{"href":1489,"rel":1490},"https:\u002F\u002Fblog.jetbrains.com\u002Fkotlin\u002F2018\u002F10\u002Fkotlin-1-3\u002F",[37],"version 1.3, en octobre 2018",", soit cinq ans avant les virtual threads. Elles ont résolu le problème bien plus tôt, au niveau du langage et du compilateur (on verra comment dans l'épisode 5), pas au niveau du runtime de la JVM.",[1484,1494,1495,1496,1499,1500,1503,1504,1507],{},"Ensuite, et c'est le point qui compte pour mon batch : les virtual threads règlent le ",[41,1497,1498],{},"coût"," des threads, pas la ",[41,1501,1502],{},"structure"," de la concurrence. Un virtual thread lancé dans le vide fuit tout autant. De l'état mutable partagé entre virtual threads produit exactement les mêmes race conditions. Ce que les coroutines apportent en plus, c'est la ",[41,1505,1506],{},"structured concurrency"," : un modèle où chaque coroutine vit dans un périmètre défini, avec un cycle de vie clair.",[18,1509,1510,1511,1516,1517,1522,1523,1528,1529,1534,1535,1540,1541,1546,1547,1550],{},"Côté Java, cette fonctionnalité existe aussi, mais elle prend son temps pour arriver. Introduite en preview dès le JDK 21 (",[33,1512,1515],{"href":1513,"rel":1514},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F453",[37],"JEP 453","), elle est toujours en preview en ",[33,1518,1521],{"href":1519,"rel":1520},"https:\u002F\u002Fwww.infoq.com\u002Fnews\u002F2025\u002F09\u002Fjava25-released",[37],"Java 25",", qui est pourtant la dernière LTS, sortie en septembre 2025 (cinquième preview, ",[33,1524,1527],{"href":1525,"rel":1526},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F505",[37],"JEP 505","). Le chantier se poursuit vers le JDK 26 (",[33,1530,1533],{"href":1531,"rel":1532},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F525",[37],"JEP 525",") et le JDK 27 (",[33,1536,1539],{"href":1537,"rel":1538},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F533",[37],"JEP 533","), essentiellement autour de la propagation d'exceptions. Java 25 a bien finalisé une fonctionnalité voisine, les ",[33,1542,1545],{"href":1543,"rel":1544},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F506",[37],"Scoped Values"," (le remplaçant moderne de ",[53,1548,1549],{},"ThreadLocal",", taillé pour les virtual threads et la structured concurrency), mais la structured concurrency elle-même n'est toujours pas stable. Côté Kotlin, elle l'est depuis 2018.",[18,1552,1553],{},"Les virtual threads auraient rendu mon batch plus économe en mémoire, mais ils ne l'auraient pas empêché de garder des fantômes de jobs précédents. C'est la structure qui manquait, pas la puissance.",[25,1555,1557],{"id":1556},"en-un-coup-dœil","En un coup d'œil",[1559,1560,1561,1579],"table",{},[1562,1563,1564],"thead",{},[1565,1566,1567,1570,1573,1576],"tr",{},[1568,1569],"th",{},[1568,1571,1572],{},"Threads Java classiques",[1568,1574,1575],{},"Virtual threads (JDK 21)",[1568,1577,1578],{},"Coroutines Kotlin",[1580,1581,1582,1597,1611,1625,1639,1653,1666,1680,1694],"tbody",{},[1565,1583,1584,1588,1591,1594],{},[1585,1586,1587],"td",{},"Unité de concurrence",[1585,1589,1590],{},"Thread OS, lourd",[1585,1592,1593],{},"Thread léger sur le tas",[1585,1595,1596],{},"Unité de travail suspendable",[1565,1598,1599,1602,1605,1608],{},[1585,1600,1601],{},"Ordonnancement",[1585,1603,1604],{},"préemptif, par l'OS",[1585,1606,1607],{},"par la JVM, au blocage",[1585,1609,1610],{},"coopératif, aux points de suspension",[1565,1612,1613,1616,1619,1622],{},[1585,1614,1615],{},"Coût mémoire unitaire",[1585,1617,1618],{},"~1 Mo de pile réservée",[1585,1620,1621],{},"~1 Ko de heap, variable",[1585,1623,1624],{},"quelques centaines d'octets",[1565,1626,1627,1630,1633,1636],{},[1585,1628,1629],{},"Écrire de l'asynchrone",[1585,1631,1632],{},"CompletableFuture , callbacks",[1585,1634,1635],{},"code bloquant, démonté par la JVM",[1585,1637,1638],{},"suspend , code séquentiel",[1565,1640,1641,1644,1647,1650],{},[1585,1642,1643],{},"Lancer en parallèle",[1585,1645,1646],{},"ExecutorService  +  Future",[1585,1648,1649],{},"executor virtuel +  Future",[1585,1651,1652],{},"coroutineScope  +  async  \u002F  awaitAll",[1565,1654,1655,1658,1661,1663],{},[1585,1656,1657],{},"Cycle de vie \u002F structure",[1585,1659,1660],{},"à gérer à la main",[1585,1662,1660],{},[1585,1664,1665],{},"structured concurrency intégrée",[1565,1667,1668,1671,1674,1677],{},[1585,1669,1670],{},"Structured concurrency",[1585,1672,1673],{},"absente",[1585,1675,1676],{},"en preview depuis JDK 21",[1585,1678,1679],{},"stable depuis 2018",[1565,1681,1682,1685,1688,1691],{},[1585,1683,1684],{},"Annulation",[1585,1686,1687],{},"interruption de thread, bancale",[1585,1689,1690],{},"interruption de thread",[1585,1692,1693],{},"coopérative et structurée",[1565,1695,1696,1699,1702,1705],{},[1585,1697,1698],{},"Disponible depuis",[1585,1700,1701],{},"Java 1.0 (1996)",[1585,1703,1704],{},"septembre 2023 (JDK 21)",[1585,1706,1707],{},"octobre 2018 (Kotlin 1.3)",[1481,1709,1710],{},[1484,1711,1712,1713,1717,1718,1723,1724,1727],{},"Les trois totaux ne viennent pas de la même source. Les chiffres threads et coroutines sont ceux de la ",[33,1714,1716],{"href":1258,"rel":1715},[37],"doc Kotlin",", donnés en ordre de grandeur large. Oracle ne publie pas d'équivalent pour les virtual threads, alors la valeur vient de ",[33,1719,1722],{"href":1720,"rel":1721},"https:\u002F\u002Fmedium.com\u002F@ManideepChinthareddy\u002Fvirtual-threads-vs-platform-threads-in-java-a-practical-performance-memory-analysis-ba38ec6500a1",[37],"mesures indépendantes"," sur des tâches à pile courte, autour de 750 octets de tas par thread. La ",[33,1725,1390],{"href":1388,"rel":1726},[37]," précise que la pile d'un virtual thread vit sur le tas et se dimensionne à la profondeur d'appel réelle, donc une requête bloquée au fond d'une stack applicative coûte plusieurs kilo-octets. Retenez le coût par unité plutôt que le total, puisqu'un virtual thread et une coroutine se tiennent dans le même ordre de grandeur, loin devant le mégaoctet réservé par un thread OS.",[25,1729,1731],{"id":1730},"à-retenir","À retenir",[1481,1733,1734,1741,1749,1752,1755,1758],{},[1484,1735,1736,1737,1740],{},"Une coroutine n'est pas un thread : c'est une unité de travail que l'on peut ",[41,1738,1739],{},"suspendre et reprendre"," sans bloquer de thread OS.",[1484,1742,1743,1745,1746,1748],{},[53,1744,437],{}," suspend, ",[53,1747,327],{}," bloque.",[1484,1750,1751],{},"L'OS préempte les threads quand il veut. Une coroutine coopère et rend la main aux points de suspension. C'est ce qui la rend efficace sur de l'i\u002Fo et piégeuse sur du calcul pur.",[1484,1753,1754],{},"Les coroutines apportent de la concurrence. Le parallélisme dépend du dispatcher et du nombre de cœurs disponibles.",[1484,1756,1757],{},"On peut faire tourner des centaines de milliers de coroutines sur une poignée de threads, là où autant de threads feraient exploser la mémoire.",[1484,1759,1760],{},"Les virtual threads Java (JDK 21) règlent le coût des threads, mais pas l'organisation de la concurrence. Le vrai apport des coroutines, c'est la structured concurrency, que Java n'a toujours pas stabilisée, même dans la LTS 25.",[18,1762,1763],{},"Dans l'épisode 2, on attaque justement cette structure : scopes, Job, builders et dispatchers, pour ne plus jamais fuiter une coroutine (ni garder de fantômes en mémoire).",[1765,1766],"hr",{},[25,1768,1770],{"id":1769},"sources","Sources",[1481,1772,1773,1783,1793,1803,1813,1823,1839,1849,1858,1867,1876,1885],{},[1484,1774,1775,1776,1779,1780],{},"Kotlin, ",[21,1777,1778],{},"Coroutines basics"," (exemple des 50 000 coroutines, comparaison mémoire) : ",[33,1781,1258],{"href":1258,"rel":1782},[37],[1484,1784,1785,1786,1789,1790],{},"JetBrains, ",[21,1787,1788],{},"Kotlin 1.3 released"," (coroutines stables, octobre 2018) : ",[33,1791,1489],{"href":1489,"rel":1792},[37],[1484,1794,1795,1796,1799,1800],{},"Oracle, ",[21,1797,1798],{},"Processes and Threads"," (process = mémoire isolée, thread partage la mémoire du process) : ",[33,1801,226],{"href":226,"rel":1802},[37],[1484,1804,1805,1806,1809,1810],{},"Intel, ",[21,1807,1808],{},"What Is Hyper-Threading?"," (un cœur physique = deux processeurs logiques \u002F threads matériels) : ",[33,1811,280],{"href":280,"rel":1812},[37],[1484,1814,1815,1816,1819,1820],{},"OpenJDK, ",[21,1817,1818],{},"JEP 444: Virtual Threads"," (finalisé en JDK 21) : ",[33,1821,1388],{"href":1388,"rel":1822},[37],[1484,1824,1795,1825,1828,1829,874,1832,1834,1835],{},[21,1826,1827],{},"Virtual Threads"," (guide JavaSE 21, API ",[53,1830,1831],{},"Thread.ofVirtual",[53,1833,1423],{},") : ",[33,1836,1837],{"href":1837,"rel":1838},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fcore\u002Fvirtual-threads.html",[37],[1484,1840,1841,1842,1845,1846],{},"InfoQ, ",[21,1843,1844],{},"Java 21 released"," (disponibilité générale le 19 septembre 2023) : ",[33,1847,1382],{"href":1382,"rel":1848},[37],[1484,1850,1841,1851,1854,1855],{},[21,1852,1853],{},"Java 25 released"," (dernière LTS, septembre 2025) : ",[33,1856,1519],{"href":1519,"rel":1857},[37],[1484,1859,1815,1860,1863,1864],{},[21,1861,1862],{},"JEP 453: Structured Concurrency (Preview)"," (introduite en preview en JDK 21) : ",[33,1865,1513],{"href":1513,"rel":1866},[37],[1484,1868,1815,1869,1872,1873],{},[21,1870,1871],{},"JEP 505: Structured Concurrency (Fifth Preview)"," (toujours en preview en JDK 25) : ",[33,1874,1525],{"href":1525,"rel":1875},[37],[1484,1877,1815,1878,1881,1882],{},[21,1879,1880],{},"JEP 506: Scoped Values"," (finalisé en JDK 25) : ",[33,1883,1543],{"href":1543,"rel":1884},[37],[1484,1886,1815,1887,1890,1891,1894,1895],{},[21,1888,1889],{},"JEP 525 \u002F JEP 533: Structured Concurrency"," (re-previews vers JDK 26 et 27) : ",[33,1892,1531],{"href":1531,"rel":1893},[37]," · ",[33,1896,1537],{"href":1537,"rel":1897},[37],[1899,1900,1901],"style",{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}",{"title":76,"searchDepth":90,"depth":90,"links":1903},[1904,1905,1906,1907,1908,1909,1911,1912,1913,1914,1915,1916,1917],{"id":27,"depth":90,"text":28},{"id":212,"depth":90,"text":213},{"id":309,"depth":90,"text":310},{"id":346,"depth":90,"text":347},{"id":381,"depth":90,"text":382},{"id":501,"depth":90,"text":1910},"suspend vu de l'extérieur",{"id":637,"depth":90,"text":638},{"id":758,"depth":90,"text":759},{"id":1252,"depth":90,"text":1253},{"id":1371,"depth":90,"text":1372},{"id":1556,"depth":90,"text":1557},{"id":1730,"depth":90,"text":1731},{"id":1769,"depth":90,"text":1770},"2026-08-13T16:35:13.002Z","Premier épisode d'une série sur les coroutines Kotlin. On part de définitions puis on regardera sous le capot. Fil rouge tout du long : la comparaison avec les threads Java.   Lors d’une mission précé","md",".\u002Fassets\u002Fcover-image.webp",{},"\u002Fblogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15",[1925,1930],{"id":1926,"name":1927,"image":1928,"linkedin":13,"x":13,"jobTitle":1929},"188f4462-cd38-80d5-b9e6-ec28a94d11e5","Bastien Dufour",".\u002Fassets\u002Freviewer-bastien-dufour.webp","Senior Software Engineer",{"id":1931,"name":1932,"image":1933,"linkedin":1934,"x":13},"37bf4462-cd38-8041-92af-c7e51809adcc","Clement Godet",".\u002Fassets\u002Freviewer-clement-godet.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fcgodet59\u002F",{"title":5,"description":1919},"blogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Findex",[75,774,1938],"craft","Qa9XrWOw_cX4JFGM1VoP29BbrwBmuJ728oDPNZ0h8Zw",[1941,1953,1959,1966],{"path":1942,"title":1943,"image":1944,"date":1945,"tags":1946},"\u002Fblogs\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code","Le vert ne suffit pas : 3 façons de douter de son code","\u002Fcontent-assets\u002F2026-06-23-le-vert-ne-suffit-pas-3-facons-de-douter-de-son-code\u002Fassets\u002Fcover-image.webp","23 juin 2026",[1938,1947,1948,774,1949,75,1950,1951,1952],"test","testing","rex","événement","2026","veille tech",{"path":1954,"title":1955,"image":1956,"date":1957,"tags":1958},"\u002Fblogs\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest","Property based testing : comment bien choisir ses propriétés avec Kotest","\u002Fcontent-assets\u002F2026-07-28-property-based-testing-comment-bien-choisir-ses-proprietes-avec-kotest\u002Fassets\u002Fcover-image.webp","28 juillet 2026",[1938,75,1947,1948],{"path":1960,"title":1961,"image":1962,"date":1963,"tags":1964},"\u002Fblogs\u002F2026-06-30-tdd-jen-entends-beaucoup-parler-mais-cest-quoi-au-juste","TDD : J’en entends beaucoup parler, mais c’est quoi au juste ?","\u002Fcontent-assets\u002F2026-06-30-tdd-jen-entends-beaucoup-parler-mais-cest-quoi-au-juste\u002Fassets\u002Fcover-image.webp","30 juin 2026",[1948,1938,1965,774,1947],"tdd",{"path":1967,"title":1968,"image":1969,"date":1970,"tags":1971},"\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide","Du prototype à la prod : ce qu'on ne te dit pas sur la construction d'une solution IA solide","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fcover-image.webp","9 juin 2026",[1972,1947,1948,1973,1974,774,1975,1938],"ia","observabilité","typescript","architecture",1786639109754]