[{"data":1,"prerenderedAt":1477},["ShallowReactive",2],{"blog-post-\u002Fblogs\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25":3,"related-\u002Fblogs\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25":1445},{"id":4,"title":5,"alt":6,"authors":7,"body":14,"date":1427,"description":1428,"extension":1429,"image":1430,"meta":1431,"navigation":712,"ogImage":1430,"path":1432,"published":712,"reviewers":1433,"seo":1440,"stem":1441,"tags":1442,"__hash__":1444},"blogs\u002Fblogs\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25\u002Findex.md","Structured concurrency, ou comment ne jamais fuiter une coroutine : Coroutines (2\u002F5)","Couverture HoppR : paysage forestier stylisé avec arbres et coucher de soleil, en teintes vertes et dorées, illustrant le concept d'arbre de jobs dans la structured concurrency - Kotlin Coroutines 2\u002F5",[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":1412},"minimark",[17,24,29,50,56,62,66,73,88,209,219,222,226,237,244,249,252,271,275,278,287,327,344,408,431,446,450,468,527,530,562,576,580,598,675,678,793,803,810,818,927,930,953,957,975,989,1112,1141,1147,1153,1157,1237,1241,1292,1307,1310,1314,1408],[18,19,20],"p",{},[21,22,23],"em",{},"Deuxième épisode de la série. Dans le premier, on a posé le modèle : une coroutine n'est pas un thread. Ici on attaque le socle qui rend les coroutines sûres en production. Fil rouge toujours en place : la comparaison avec Java.",[25,26,28],"h2",{"id":27},"le-vrai-problème-de-mon-batch","Le vrai problème de mon batch",[18,30,31,32,39,40,44,45,49],{},"Retour sur le batch Spring Batch de ",[33,34,38],"a",{"href":35,"rel":36},"https:\u002F\u002Fblog.hoppr.tech\u002Fblogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15",[37],"nofollow","l'épisode 1",". Des ",[41,42,43],"code",{},"Future"," lancés un peu partout, de l'état mutable partagé et des données d'un job précédent qui polluaient le suivant. J'avais conclu que le coupable n'était pas le nombre de threads, mais l'",[46,47,48],"strong",{},"absence de structure",".",[18,51,52,53,55],{},"Voici ce que cet épisode adresse: quand je lance un traitement concurrent, qui est responsable de son cycle de vie ? Qui l'attend ? Qui l'annule si une partie tombe en erreur ? Dans mon batch, la réponse était “personne”. Chaque ",[41,54,43],{}," vivait sa vie, et l'application n'avait aucun moyen de dire “arrête tout, on recommence proprement“.",[18,57,58,59,49],{},"La réponse de Kotlin à cette question, c'est la ",[46,60,61],{},"structured concurrency",[25,63,65],{"id":64},"doù-vient-la-structure-le-scope","D'où vient la structure : le scope",[18,67,68,69,72],{},"Une coroutine ne se lance pas dans le vide : elle se lance dans un ",[46,70,71],{},"scope",", qui borne son cycle de vie. Tant que des coroutines tournent dans un scope, ce scope n'est pas terminé. Quand on annule le scope, toutes les coroutines de ce scope sont annulées.",[18,74,75,76,83,84,87],{},"La brique de base, c'est ",[33,77,80],{"href":78,"rel":79},"https:\u002F\u002Fkotlinlang.org\u002Fapi\u002Fkotlinx.coroutines\u002Fkotlinx-coroutines-core\u002Fkotlinx.coroutines\u002F-coroutine-scope\u002F",[37],[41,81,82],{},"coroutineScope",". Elle crée un scope et ne rend la main que lorsque ",[46,85,86],{},"toutes"," les coroutines lancées à l'intérieur sont terminées :",[89,90,95],"pre",{"className":91,"code":92,"language":93,"meta":94,"style":94},"language-kotlin shiki shiki-themes github-dark-default","suspend fun loadDashboard(): Dashboard = coroutineScope {\n    val stats  = async { api.getStats() }    \u002F\u002F lancé en parallèle\n    val alerts = async { api.getAlerts() }   \u002F\u002F lancé en parallèle\n    Dashboard(stats.await(), alerts.await())\n} \u002F\u002F ne rend la main que quand les deux coroutines ont finis\n","kotlin","",[41,96,97,130,158,180,200],{"__ignoreMap":94},[98,99,102,106,109,113,117,121,124,127],"span",{"class":100,"line":101},"line",1,[98,103,105],{"class":104},"suJrU","suspend",[98,107,108],{"class":104}," fun",[98,110,112],{"class":111},"sc3cj"," loadDashboard",[98,114,116],{"class":115},"sZEs4","(): ",[98,118,120],{"class":119},"sQhOw","Dashboard",[98,122,123],{"class":104}," =",[98,125,126],{"class":111}," coroutineScope",[98,128,129],{"class":115}," {\n",[98,131,133,136,139,142,145,148,151,154],{"class":100,"line":132},2,[98,134,135],{"class":104},"    val",[98,137,138],{"class":115}," stats  ",[98,140,141],{"class":104},"=",[98,143,144],{"class":111}," async",[98,146,147],{"class":115}," { api.",[98,149,150],{"class":111},"getStats",[98,152,153],{"class":115},"() }    ",[98,155,157],{"class":156},"sH3jZ","\u002F\u002F lancé en parallèle\n",[98,159,161,163,166,168,170,172,175,178],{"class":100,"line":160},3,[98,162,135],{"class":104},[98,164,165],{"class":115}," alerts ",[98,167,141],{"class":104},[98,169,144],{"class":111},[98,171,147],{"class":115},[98,173,174],{"class":111},"getAlerts",[98,176,177],{"class":115},"() }   ",[98,179,157],{"class":156},[98,181,183,186,189,192,195,197],{"class":100,"line":182},4,[98,184,185],{"class":111},"    Dashboard",[98,187,188],{"class":115},"(stats.",[98,190,191],{"class":111},"await",[98,193,194],{"class":115},"(), alerts.",[98,196,191],{"class":111},[98,198,199],{"class":115},"())\n",[98,201,203,206],{"class":100,"line":202},5,[98,204,205],{"class":115},"} ",[98,207,208],{"class":156},"\u002F\u002F ne rend la main que quand les deux coroutines ont finis\n",[18,210,211,212,215,216,218],{},"C'est cette structure qu’apporte les coroutines: impossible que ",[41,213,214],{},"loadDashboard"," retourne pendant qu'une de ses coroutines tourne encore. Et si l'appelant annule ",[41,217,214],{},", les deux appels réseau sont annulés avec. On ne peut pas fuiter une coroutine par accident, parce qu'il n'existe pas de “coroutine orpheline” dans ce modèle.",[18,220,221],{},"C'est exactement ce qui manquait à mon batch : un périmètre clair où “tout ce qui a été lancé ici est attendu ici”.",[25,223,225],{"id":224},"les-jobs-un-arbre-qui-seffondre-ou-se-négocie","Les jobs : un arbre qui s'effondre ou se négocie",[18,227,228,229,232,233,236],{},"Sous le capot, chaque coroutine est représentée par un ",[41,230,231],{},"Job",". Les jobs forment un ",[46,234,235],{},"arbre"," : la coroutine qui en lance une autre en devient le parent (l'épisode 5 explique les détails d'implémentation des jobs). Selon le type de scope, l'erreur d'un enfant peut soit annuler tous ses frères, soit les laisser continuer.",[18,238,239],{},[240,241],"img",{"alt":242,"src":243},"Diagramme comparatif : à gauche, coroutineScope où l'erreur d'un enfant annule ses frères (flèches rouges) ; à droite, supervisorScope où l'erreur d'un enfant isole sa défaillance (flèches vertes)","\u002Fcontent-assets\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25\u002Fassets\u002Fimg1.webp",[18,245,246],{},[46,247,248],{},"Le choix entre les deux se résume à une question simple : l'échec d'une sous-tâche doit-il condamner les autres ?",[18,250,251],{},"En pratique :",[253,254,255,263],"ul",{},[256,257,258,262],"li",{},[46,259,260],{},[41,261,82],{}," quand tout est critique. Une agrégation requiert profil + commandes + reco : si le profil échoue, inutile d'attendre les autres.",[256,264,265,270],{},[46,266,267],{},[41,268,269],{},"supervisorScope"," quand on tolère les défaillances partielles. Un écran charge cinq widgets : l'échec d'un ne doit pas vider les autres.",[25,272,274],{"id":273},"launch-vs-async-attendre-ou-ignorer","Launch vs async : attendre ou ignorer",[18,276,277],{},"Pour démarrer une coroutine dans un scope, deux builders principaux, selon qu'on attend un résultat ou non.",[18,279,280,283,284,286],{},[41,281,282],{},"launch"," lance un traitement sans valeur de retour. Il rend un ",[41,285,231],{},", qu'on peut annuler ou attendre, mais qui ne porte pas de résultat. C'est le fire-and-forget structuré.",[89,288,290],{"className":91,"code":289,"language":93,"meta":94,"style":94},"val job: Job = scope.launch {\n    sendMetrics()   \u002F\u002F pas de résultat attendu\n}\n",[41,291,292,311,322],{"__ignoreMap":94},[98,293,294,297,300,302,304,307,309],{"class":100,"line":101},[98,295,296],{"class":104},"val",[98,298,299],{"class":115}," job: ",[98,301,231],{"class":119},[98,303,123],{"class":104},[98,305,306],{"class":115}," scope.",[98,308,282],{"class":111},[98,310,129],{"class":115},[98,312,313,316,319],{"class":100,"line":132},[98,314,315],{"class":111},"    sendMetrics",[98,317,318],{"class":115},"()   ",[98,320,321],{"class":156},"\u002F\u002F pas de résultat attendu\n",[98,323,324],{"class":100,"line":160},[98,325,326],{"class":115},"}\n",[18,328,329,332,333,336,337,340,341,49],{},[41,330,331],{},"async"," lance un traitement qui ",[46,334,335],{},"produit une valeur",". Il rend un ",[41,338,339],{},"Deferred\u003CT>",", une promesse dont on récupère le résultat avec ",[41,342,343],{},"await()",[89,345,347],{"className":91,"code":346,"language":93,"meta":94,"style":94},"val deferred: Deferred\u003CInt> = scope.async { \n        computeScore() \n}\nval score: Int = deferred.await()\n",[41,348,349,377,385,389],{"__ignoreMap":94},[98,350,351,353,356,359,362,365,368,370,372,374],{"class":100,"line":101},[98,352,296],{"class":104},[98,354,355],{"class":115}," deferred: ",[98,357,358],{"class":119},"Deferred",[98,360,361],{"class":115},"\u003C",[98,363,364],{"class":119},"Int",[98,366,367],{"class":115},"> ",[98,369,141],{"class":104},[98,371,306],{"class":115},[98,373,331],{"class":111},[98,375,376],{"class":115}," { \n",[98,378,379,382],{"class":100,"line":132},[98,380,381],{"class":111},"        computeScore",[98,383,384],{"class":115},"() \n",[98,386,387],{"class":100,"line":160},[98,388,326],{"class":115},[98,390,391,393,396,398,400,403,405],{"class":100,"line":182},[98,392,296],{"class":104},[98,394,395],{"class":115}," score: ",[98,397,364],{"class":119},[98,399,123],{"class":104},[98,401,402],{"class":115}," deferred.",[98,404,191],{"class":111},[98,406,407],{"class":115},"()\n",[18,409,410,411,413,414,416,417,419,420,423,424,426,427,430],{},"Pour paralléliser plusieurs tâches : lancer plusieurs ",[41,412,331],{}," ensemble, puis synchroniser les résultats avec ",[41,415,191],{},". C'est ce qu'on a vu dans ",[41,418,214],{},". Deux appels réseau partent en même temps, et on attend les deux. Comparé au batch et à ses ",[41,421,422],{},"List\u003CFuture>"," collectées à la main, la différence tient dans le fait que ces ",[41,425,331],{}," sont ",[46,428,429],{},"liés au scope"," : si l'un échoue, l'autre est annulé, et rien ne survit au bloc.",[18,432,433,434,437,438,441,442,445],{},"Reste ",[41,435,436],{},"runBlocking",", mentionné dans l'épisode 1. Lui fait le pont inverse : il ",[46,439,440],{},"bloque"," le thread courant jusqu'à la fin des coroutines qu'il contient. Pratique pour un ",[41,443,444],{},"main"," ou un test, à éviter en plein code applicatif justement parce qu'il bloque un thread au lieu de le libérer.",[25,447,449],{"id":448},"choisir-son-dispatcher-cpu-io-ou-ui","Choisir son dispatcher : CPU, IO, ou UI",[18,451,452,453,456,457,460,461,464,465,49],{},"Un scope dit ",[21,454,455],{},"quand"," les coroutines vivent. Un ",[46,458,459],{},"dispatcher"," dit ",[21,462,463],{},"sur quel thread"," elles s'exécutent. On le choisit en le passant au builder, ou via ",[41,466,467],{},"withContext",[89,469,471],{"className":91,"code":470,"language":93,"meta":94,"style":94},"launch(Dispatchers.Default) { sortLargeList() }      \u002F\u002F calcul CPU\nlaunch(Dispatchers.IO)      { file.readText() }       \u002F\u002F I\u002FO bloquant\nwithContext(Dispatchers.Main) { view.display(data) }  \u002F\u002F thread UI\n",[41,472,473,489,505],{"__ignoreMap":94},[98,474,475,477,480,483,486],{"class":100,"line":101},[98,476,282],{"class":111},[98,478,479],{"class":115},"(Dispatchers.Default) { ",[98,481,482],{"class":111},"sortLargeList",[98,484,485],{"class":115},"() }      ",[98,487,488],{"class":156},"\u002F\u002F calcul CPU\n",[98,490,491,493,496,499,502],{"class":100,"line":132},[98,492,282],{"class":111},[98,494,495],{"class":115},"(Dispatchers.IO)      { file.",[98,497,498],{"class":111},"readText",[98,500,501],{"class":115},"() }       ",[98,503,504],{"class":156},"\u002F\u002F I\u002FO bloquant\n",[98,506,507,509,512,515,518,521,524],{"class":100,"line":160},[98,508,467],{"class":111},[98,510,511],{"class":115},"(Dispatchers.Main) { view.",[98,513,514],{"class":111},"display",[98,516,517],{"class":115},"(",[98,519,520],{"class":104},"data",[98,522,523],{"class":115},") }  ",[98,525,526],{"class":156},"\u002F\u002F thread UI\n",[18,528,529],{},"Trois dispatchers à connaître chacun taillé pour un usage différent :",[253,531,532,540,554],{},[256,533,534,539],{},[46,535,536],{},[41,537,538],{},"Dispatchers.Default"," pour le calcul intensif. Son pool de threads est borné au nombre de cœurs du processeur. Inutile d'avoir plus de threads que de cœurs quand on sature le CPU.",[256,541,542,547,548,553],{},[46,543,544],{},[41,545,546],{},"Dispatchers.IO"," pour les entrées-sorties bloquantes (fichiers, appels bdd, appels réseau bloquants). Son pool est élastique et monte ",[33,549,552],{"href":550,"rel":551},"https:\u002F\u002Fkotlinlang.org\u002Fapi\u002Fkotlinx.coroutines\u002Fkotlinx-coroutines-core\u002Fkotlinx.coroutines\u002F-dispatchers\u002F-i-o.html",[37],"jusqu'à 64 threads par défaut ou le nombre de cœurs si celui-ci est supérieur",". Comme ces tâches passent leur temps à attendre, on peut en paralléliser beaucoup plus que de cœurs.",[256,555,556,561],{},[46,557,558],{},[41,559,560],{},"Dispatchers.Main"," pour le thread d'UI (Android, app Desktop), là où on met à jour l'interface.",[18,563,564,567,568,571,572,575],{},[41,565,566],{},"Default"," et ",[41,569,570],{},"IO"," sont dimensionnés différemment ",[46,573,574],{},"parce que"," leurs charges sont différentes. On reviendra sur leur fonctionnement interne (ils partagent en réalité le même pool) dans l'épisode 5.",[25,577,579],{"id":578},"globalscope-pourquoi-cest-dangereux-et-quand-ça-va","GlobalScope : pourquoi c'est dangereux (et quand ça va)",[18,581,582,583,586,587,589,590,597],{},"Il existe un scope global, ",[41,584,585],{},"GlobalScope",", qui n'est lié à aucun cycle de vie. Une coroutine lancée dedans vit aussi longtemps qu'elle veut, sans que personne ne l'attende ni ne l'annule. C'est le retour direct au problème de mon batch. Ce n'est pas qu'un avis : ",[41,588,585],{}," est officiellement marqué ",[33,591,594],{"href":592,"rel":593},"https:\u002F\u002Fkotlinlang.org\u002Fapi\u002Fkotlinx.coroutines\u002Fkotlinx-coroutines-core\u002Fkotlinx.coroutines\u002F-global-scope\u002F",[37],[41,595,596],{},"@DelicateCoroutinesApi",", une annotation qui force un opt-in explicite pour l'utiliser, précisément pour éviter qu'on s'en serve par réflexe.",[89,599,601],{"className":91,"code":600,"language":93,"meta":94,"style":94},"\u002F\u002F À éviter : la coroutine survit à l'écran, personne ne l'attend ni ne l'annule\nfun onScreenOpen() {\n    GlobalScope.launch {\n        val data = repo.load()   \u002F\u002F continue même après la fermeture de l'écran\n        render(data)             \u002F\u002F peut planter sur une vue déjà détruite\n    }\n}\n",[41,602,603,608,619,628,649,664,670],{"__ignoreMap":94},[98,604,605],{"class":100,"line":101},[98,606,607],{"class":156},"\u002F\u002F À éviter : la coroutine survit à l'écran, personne ne l'attend ni ne l'annule\n",[98,609,610,613,616],{"class":100,"line":132},[98,611,612],{"class":104},"fun",[98,614,615],{"class":111}," onScreenOpen",[98,617,618],{"class":115},"() {\n",[98,620,621,624,626],{"class":100,"line":160},[98,622,623],{"class":115},"    GlobalScope.",[98,625,282],{"class":111},[98,627,129],{"class":115},[98,629,630,633,636,638,641,644,646],{"class":100,"line":182},[98,631,632],{"class":104},"        val",[98,634,635],{"class":104}," data",[98,637,123],{"class":104},[98,639,640],{"class":115}," repo.",[98,642,643],{"class":111},"load",[98,645,318],{"class":115},[98,647,648],{"class":156},"\u002F\u002F continue même après la fermeture de l'écran\n",[98,650,651,654,656,658,661],{"class":100,"line":202},[98,652,653],{"class":111},"        render",[98,655,517],{"class":115},[98,657,520],{"class":104},[98,659,660],{"class":115},")             ",[98,662,663],{"class":156},"\u002F\u002F peut planter sur une vue déjà détruite\n",[98,665,667],{"class":100,"line":666},6,[98,668,669],{"class":115},"    }\n",[98,671,673],{"class":100,"line":672},7,[98,674,326],{"class":115},[18,676,677],{},"La version structurée relie la coroutine à un scope dont on maîtrise la fin :",[89,679,681],{"className":91,"code":680,"language":93,"meta":94,"style":94},"class Screen(private val scope: CoroutineScope) {\n\n    fun onOpen() = scope.launch {\n        val data = repo.load()\n        render(data)\n    }\n\n    fun onClose() = scope.cancel()   \u002F\u002F annule tout ce qui est encore en vol\n}\n",[41,682,683,708,714,733,747,758,762,766,788],{"__ignoreMap":94},[98,684,685,688,691,693,696,699,702,705],{"class":100,"line":101},[98,686,687],{"class":104},"class",[98,689,690],{"class":119}," Screen",[98,692,517],{"class":115},[98,694,695],{"class":104},"private",[98,697,698],{"class":104}," val",[98,700,701],{"class":115}," scope: ",[98,703,704],{"class":119},"CoroutineScope",[98,706,707],{"class":115},") {\n",[98,709,710],{"class":100,"line":132},[98,711,713],{"emptyLinePlaceholder":712},true,"\n",[98,715,716,719,722,725,727,729,731],{"class":100,"line":160},[98,717,718],{"class":104},"    fun",[98,720,721],{"class":111}," onOpen",[98,723,724],{"class":115},"() ",[98,726,141],{"class":104},[98,728,306],{"class":115},[98,730,282],{"class":111},[98,732,129],{"class":115},[98,734,735,737,739,741,743,745],{"class":100,"line":182},[98,736,632],{"class":104},[98,738,635],{"class":104},[98,740,123],{"class":104},[98,742,640],{"class":115},[98,744,643],{"class":111},[98,746,407],{"class":115},[98,748,749,751,753,755],{"class":100,"line":202},[98,750,653],{"class":111},[98,752,517],{"class":115},[98,754,520],{"class":104},[98,756,757],{"class":115},")\n",[98,759,760],{"class":100,"line":666},[98,761,669],{"class":115},[98,763,764],{"class":100,"line":672},[98,765,713],{"emptyLinePlaceholder":712},[98,767,769,771,774,776,778,780,783,785],{"class":100,"line":768},8,[98,770,718],{"class":104},[98,772,773],{"class":111}," onClose",[98,775,724],{"class":115},[98,777,141],{"class":104},[98,779,306],{"class":115},[98,781,782],{"class":111},"cancel",[98,784,318],{"class":115},[98,786,787],{"class":156},"\u002F\u002F annule tout ce qui est encore en vol\n",[98,789,791],{"class":100,"line":790},9,[98,792,326],{"class":115},[18,794,795,796,567,799,802],{},"Les frameworks fournissent d'ailleurs des scopes déjà câblés sur un cycle de vie : ",[41,797,798],{},"viewModelScope",[41,800,801],{},"lifecycleScope"," sur Android, le scope d'une requête côté serveur.",[804,805,807,809],"h3",{"id":806},"globalscope-a-t-il-quand-même-un-usage",[41,808,585],{}," a-t-il quand même un usage ?",[18,811,812,813,817],{},"Oui un seul et il est bien spécifique. La ",[33,814,816],{"href":592,"rel":815},[37],"documentation officielle"," le réserve aux processus de fond de haut niveau qui doivent rester actifs toute la durée de vie de l'application. Typiquement une coroutine globale qui tourne du démarrage à l'arrêt, comme un logger de statistiques en tâche de fond :",[89,819,821],{"className":91,"code":820,"language":93,"meta":94,"style":94},"@OptIn(DelicateCoroutinesApi::class)\nval statsReporter = GlobalScope.launch(\n    CoroutineExceptionHandler { _, e -> logFatalError(\"stats logging: $e\") }\n) {\n    while (true) {\n        delay(1.seconds)\n        logStatistics()\n    }\n}\n",[41,822,823,835,852,882,886,899,912,919,923],{"__ignoreMap":94},[98,824,825,828,831,833],{"class":100,"line":101},[98,826,827],{"class":119},"@OptIn",[98,829,830],{"class":115},"(DelicateCoroutinesApi::",[98,832,687],{"class":111},[98,834,757],{"class":115},[98,836,837,839,842,844,847,849],{"class":100,"line":132},[98,838,296],{"class":104},[98,840,841],{"class":115}," statsReporter ",[98,843,141],{"class":104},[98,845,846],{"class":115}," GlobalScope.",[98,848,282],{"class":111},[98,850,851],{"class":115},"(\n",[98,853,854,857,860,863,866,868,872,876,879],{"class":100,"line":160},[98,855,856],{"class":111},"    CoroutineExceptionHandler",[98,858,859],{"class":115}," { _, e ",[98,861,862],{"class":104},"->",[98,864,865],{"class":111}," logFatalError",[98,867,517],{"class":115},[98,869,871],{"class":870},"s9uIt","\"stats logging: ",[98,873,875],{"class":874},"sFSAA","$e",[98,877,878],{"class":870},"\"",[98,880,881],{"class":115},") }\n",[98,883,884],{"class":100,"line":182},[98,885,707],{"class":115},[98,887,888,891,894,897],{"class":100,"line":202},[98,889,890],{"class":104},"    while",[98,892,893],{"class":115}," (",[98,895,896],{"class":874},"true",[98,898,707],{"class":115},[98,900,901,904,906,909],{"class":100,"line":666},[98,902,903],{"class":111},"        delay",[98,905,517],{"class":115},[98,907,908],{"class":874},"1",[98,910,911],{"class":115},".seconds)\n",[98,913,914,917],{"class":100,"line":672},[98,915,916],{"class":111},"        logStatistics",[98,918,407],{"class":115},[98,920,921],{"class":100,"line":768},[98,922,669],{"class":115},[98,924,925],{"class":100,"line":790},[98,926,326],{"class":115},[18,928,929],{},"Deux précautions même dans ce cas :",[253,931,932,941],{},[256,933,934,936,937,940],{},[41,935,585],{}," ne fournit aucun ",[41,938,939],{},"CoroutineExceptionHandler"," : sans en configurer un soi-même, une exception non gérée peut faire crasher l'application.",[256,942,943,944,946,947,949,950,952],{},"Beaucoup préfèrent créer un ",[41,945,704],{}," applicatif explicite plutôt que ",[41,948,585],{},", pour garder la main sur l'annulation. ",[41,951,585],{}," reste le dernier recours, pas l'outil par défaut.",[25,954,956],{"id":955},"côté-java-le-même-besoin-en-cours-de-stabilisation","Côté Java : le même besoin, en cours de stabilisation",[18,958,959,960,963,964,966,967,970,971,974],{},"Dans l'épisode 1, on a vu le style Java historique : un ",[41,961,962],{},"ExecutorService",", une ",[41,965,422],{},", un ",[41,968,969],{},"join"," à la main, un ",[41,972,973],{},"shutdown"," à ne pas oublier. Rien ne relie les tâches entre elles. C'est le monde d'avant la structured concurrency, et c'était celui de mon batch.",[18,976,977,978,981,982,985,986,988],{},"Java a bien identifié le problème et propose une réponse très proche dans l'esprit : ",[41,979,980],{},"StructuredTaskScope",". On ouvre un scope, on ",[41,983,984],{},"fork"," des sous-tâches, on ",[41,987,969],{},", et le scope garantit qu'aucune sous-tâche ne survit au bloc.",[89,990,994],{"className":991,"code":992,"language":993,"meta":94,"style":94},"language-java shiki shiki-themes github-dark-default","\u002F\u002F Java 25 (preview) : StructuredTaskScope, l'équivalent en cours de stabilisation\ntry (var scope = StructuredTaskScope.open()) {\n    var stats  = scope.fork(() -> api.getStats());\n    var alerts = scope.fork(() -> api.getAlerts());\n    scope.join();\n    return new Dashboard(stats.get(), alerts.get());\n}\n","java",[41,995,996,1001,1025,1053,1076,1086,1108],{"__ignoreMap":94},[98,997,998],{"class":100,"line":101},[98,999,1000],{"class":156},"\u002F\u002F Java 25 (preview) : StructuredTaskScope, l'équivalent en cours de stabilisation\n",[98,1002,1003,1006,1008,1011,1014,1016,1019,1022],{"class":100,"line":132},[98,1004,1005],{"class":104},"try",[98,1007,893],{"class":115},[98,1009,1010],{"class":104},"var",[98,1012,1013],{"class":115}," scope",[98,1015,123],{"class":104},[98,1017,1018],{"class":115}," StructuredTaskScope.",[98,1020,1021],{"class":111},"open",[98,1023,1024],{"class":115},"()) {\n",[98,1026,1027,1030,1033,1036,1038,1040,1043,1045,1048,1050],{"class":100,"line":160},[98,1028,1029],{"class":104},"    var",[98,1031,1032],{"class":115}," stats",[98,1034,1035],{"class":104},"  =",[98,1037,306],{"class":115},[98,1039,984],{"class":111},[98,1041,1042],{"class":115},"(() ",[98,1044,862],{"class":104},[98,1046,1047],{"class":115}," api.",[98,1049,150],{"class":111},[98,1051,1052],{"class":115},"());\n",[98,1054,1055,1057,1060,1062,1064,1066,1068,1070,1072,1074],{"class":100,"line":182},[98,1056,1029],{"class":104},[98,1058,1059],{"class":115}," alerts",[98,1061,123],{"class":104},[98,1063,306],{"class":115},[98,1065,984],{"class":111},[98,1067,1042],{"class":115},[98,1069,862],{"class":104},[98,1071,1047],{"class":115},[98,1073,174],{"class":111},[98,1075,1052],{"class":115},[98,1077,1078,1081,1083],{"class":100,"line":202},[98,1079,1080],{"class":115},"    scope.",[98,1082,969],{"class":111},[98,1084,1085],{"class":115},"();\n",[98,1087,1088,1091,1094,1097,1099,1102,1104,1106],{"class":100,"line":666},[98,1089,1090],{"class":104},"    return",[98,1092,1093],{"class":104}," new",[98,1095,1096],{"class":111}," Dashboard",[98,1098,188],{"class":115},[98,1100,1101],{"class":111},"get",[98,1103,194],{"class":115},[98,1105,1101],{"class":111},[98,1107,1052],{"class":115},[98,1109,1110],{"class":100,"line":672},[98,1111,326],{"class":115},[18,1113,1114,1115,1117,1118,1123,1124,567,1129,1134,1135,1137,1138,1140],{},"La ressemblance avec ",[41,1116,82],{}," est forte : les deux écosystèmes convergent vers le même modèle. Mais pas au même rythme. Comme vu dans l'épisode 1, cette API est ",[33,1119,1122],{"href":1120,"rel":1121},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F505",[37],"encore en preview dans Java 25",", la dernière LTS, et continue d'évoluer vers les ",[33,1125,1128],{"href":1126,"rel":1127},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F525",[37],"JDK 26",[33,1130,1133],{"href":1131,"rel":1132},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F533",[37],"27",". tandis que Kotlin a stabilisé ",[41,1136,82],{},", ",[41,1139,231],{}," et les scopes depuis 2018.",[18,1142,1143],{},[240,1144],{"alt":1145,"src":1146},"Meme humoristique : \"Well... we're waiting\" - illustrant que Java attend toujours la stabilisation de StructuredTaskScope tandis que Kotlin a livré depuis 2018","\u002Fcontent-assets\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25\u002Fassets\u002Fimg2.webp",[18,1148,1149,1150,1152],{},"Si mon batch avait été écrit en Kotlin avec un ",[41,1151,82],{},", le bug des “fantômes en mémoire” aurait été bien plus difficile à écrire. Le modèle refuse par construction les coroutines orphelines.",[25,1154,1156],{"id":1155},"en-un-coup-dœil","En un coup d'œil",[1158,1159,1160,1173],"table",{},[1161,1162,1163],"thead",{},[1164,1165,1166,1170],"tr",{},[1167,1168,1169],"th",{},"Besoin",[1167,1171,1172],{},"Outil Kotlin",[1174,1175,1176,1185,1193,1201,1209,1216,1223,1230],"tbody",{},[1164,1177,1178,1182],{},[1179,1180,1181],"td",{},"Lancer des coroutines et toutes les attendre",[1179,1183,1184],{},"coroutineScope { }",[1164,1186,1187,1190],{},[1179,1188,1189],{},"Isoler l'échec d'un enfant",[1179,1191,1192],{},"supervisorScope { }  \u002F  SupervisorJob",[1164,1194,1195,1198],{},[1179,1196,1197],{},"Lancer sans résultat (fire-and-forget)",[1179,1199,1200],{},"launch  →  Job",[1164,1202,1203,1206],{},[1179,1204,1205],{},"Lancer et récupérer un résultat",[1179,1207,1208],{},"async  →  Deferred  →  await()",[1164,1210,1211,1214],{},[1179,1212,1213],{},"Calcul CPU",[1179,1215,538],{},[1164,1217,1218,1221],{},[1179,1219,1220],{},"I\u002FO bloquant",[1179,1222,546],{},[1164,1224,1225,1228],{},[1179,1226,1227],{},"Thread UI",[1179,1229,560],{},[1164,1231,1232,1235],{},[1179,1233,1234],{},"Lancer du bloquant (main, test)",[1179,1236,436],{},[25,1238,1240],{"id":1239},"à-retenir","À retenir",[253,1242,1243,1249,1257,1271,1284],{},[256,1244,1245,1246,1248],{},"Une coroutine se lance toujours dans un ",[46,1247,71],{},", qui borne sa vie. Pas de coroutine orpheline.",[256,1250,1251,1253,1254,1256],{},[41,1252,82],{}," attend tous ses enfants ; l'échec d'un enfant annule ses frères. ",[41,1255,269],{}," isole les échecs.",[256,1258,1259,1261,1262,1264,1265,1267,1268,1270],{},[41,1260,282],{}," pour un effet, ",[41,1263,331],{}," + ",[41,1266,191],{}," pour un résultat. ",[41,1269,436],{}," seulement aux frontières.",[256,1272,1273,1274,1276,1277,1279,1280,1283],{},"Choisir le dispatcher selon la charge : ",[41,1275,566],{}," (CPU), ",[41,1278,570],{}," (entrées-sorties), ",[41,1281,1282],{},"Main"," (UI).",[256,1285,1286,1288,1289,1291],{},[41,1287,585],{}," casse la structure (API ",[41,1290,596],{},") : à réserver aux rares tâches qui doivent vivre toute la durée de l'application, jamais par défaut.",[18,1293,1294,1295,1298,1299,1302,1303,1306],{},"Dans l'épisode 3, on regarde ce qui se passe quand ça tourne mal : l'",[46,1296,1297],{},"annulation coopérative"," et la ",[46,1300,1301],{},"gestion d'exceptions",", y compris le piège classique du ",[41,1304,1305],{},"try\u002Fcatch"," qui avale une annulation.",[1308,1309],"hr",{},[25,1311,1313],{"id":1312},"sources","Sources",[253,1315,1316,1327,1342,1352,1366,1375,1385,1395],{},[256,1317,1318,1319,1322,1323],{},"Kotlin, ",[21,1320,1321],{},"Coroutines basics"," (structured concurrency, scope) : ",[33,1324,1325],{"href":1325,"rel":1326},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fcoroutines-basics.html",[37],[256,1328,1318,1329,893,1332,1334,1335,1337,1338],{},[21,1330,1331],{},"Composing suspending functions",[41,1333,331],{}," \u002F ",[41,1336,191],{},") : ",[33,1339,1340],{"href":1340,"rel":1341},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fcomposing-suspending-functions.html",[37],[256,1343,1318,1344,1347,1348],{},[21,1345,1346],{},"Coroutine context and dispatchers"," : ",[33,1349,1350],{"href":1350,"rel":1351},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fcoroutine-context-and-dispatchers.html",[37],[256,1353,1318,1354,893,1357,1359,1360,1337,1362],{},[21,1355,1356],{},"Coroutine exceptions handling",[41,1358,82],{}," vs ",[41,1361,269],{},[33,1363,1364],{"href":1364,"rel":1365},"https:\u002F\u002Fkotlinlang.org\u002Fdocs\u002Fexception-handling.html",[37],[256,1367,1368,1369,1371,1372],{},"Kotlin API, ",[21,1370,546],{}," (limite de 64 threads) : ",[33,1373,550],{"href":550,"rel":1374},[37],[256,1376,1368,1377,893,1379,1381,1382],{},[21,1378,585],{},[41,1380,596],{},", cas d'usage légitime) : ",[33,1383,592],{"href":592,"rel":1384},[37],[256,1386,1387,1388,1391,1392],{},"OpenJDK, ",[21,1389,1390],{},"JEP 505: Structured Concurrency (Fifth Preview)"," (Java 25) : ",[33,1393,1120],{"href":1120,"rel":1394},[37],[256,1396,1387,1397,1400,1401,1404,1405],{},[21,1398,1399],{},"JEP 525 \u002F JEP 533"," (re-previews vers JDK 26 et 27) : ",[33,1402,1126],{"href":1126,"rel":1403},[37]," · ",[33,1406,1131],{"href":1131,"rel":1407},[37],[1409,1410,1411],"style",{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}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 .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}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 .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}",{"title":94,"searchDepth":132,"depth":132,"links":1413},[1414,1415,1416,1417,1418,1419,1423,1424,1425,1426],{"id":27,"depth":132,"text":28},{"id":64,"depth":132,"text":65},{"id":224,"depth":132,"text":225},{"id":273,"depth":132,"text":274},{"id":448,"depth":132,"text":449},{"id":578,"depth":132,"text":579,"children":1420},[1421],{"id":806,"depth":160,"text":1422},"GlobalScope a-t-il quand même un usage ?",{"id":955,"depth":132,"text":956},{"id":1155,"depth":132,"text":1156},{"id":1239,"depth":132,"text":1240},{"id":1312,"depth":132,"text":1313},"2026-09-08T07:07:31.631Z","Deuxième épisode de la série. Dans le premier, on a posé le modèle : une coroutine n'est pas un thread. Ici on attaque le socle qui rend les coroutines sûres en production. Fil rouge toujours en place","md",".\u002Fassets\u002Fcover-image.webp",{},"\u002Fblogs\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25",[1434],{"id":1435,"name":1436,"image":1437,"linkedin":1438,"x":13,"jobTitle":1439},"e8163b24-7e01-41c5-adbf-0dc655f929d0","Nicolas Zago",".\u002Fassets\u002Freviewer-nicolas-zago.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fnicolaszago\u002F","CEO HoppR",{"title":5,"description":1428},"blogs\u002F2026-09-08-structured-concurrency-ou-comment-ne-jamais-fuiter-une-coroutine-coroutines-25\u002Findex",[93,1443,993],"craft","GfANBf04ssBUsGLt5sxiPFPhWMoKVrRXyunRKa4yMFM",[1446,1452,1464,1470],{"path":1447,"title":1448,"image":1449,"date":1450,"tags":1451},"\u002Fblogs\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15","Une coroutine n'est pas un thread : Coroutines (1\u002F5)","\u002Fcontent-assets\u002F2026-08-13-une-coroutine-nest-pas-un-thread-coroutines-15\u002Fassets\u002Fcover-image.webp","13 août 2026",[93,993,1443],{"path":1453,"title":1454,"image":1455,"date":1456,"tags":1457},"\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",[1443,1458,1459,993,1460,93,1461,1462,1463],"test","testing","rex","événement","2026","veille tech",{"path":1465,"title":1466,"image":1467,"date":1468,"tags":1469},"\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",[1443,93,1458,1459],{"path":1471,"title":1472,"image":1473,"date":1474,"tags":1475},"\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",[1459,1443,1476,993,1458],"tdd",1788851412213]