Ce serait effectivement une manière assez plus maladroite de faire! ;)
Oui, mais pourtant, je l'ai déjà vu plusieurs fois sur des projets nodejs en production. Ce n'est pas toujours facile de voir à l'avance où ça va se produire. Et même quand on le sait, ce n'est pas facile à transformer. Ça demande d'une part de modifier du code synchrone en code asynchrone (normalement, ce n'est pas très compliqué, c'est juste du travail de bucheron) et, d'autre part, il faut réussir à introduire le découpage qui va bien (là, c'est plus compliqué).
Si on prend ton exemple, et que l'on ajoute la version bloquante pour comparer :
consthttp=require('http');constprocess=require('process');constcrypto=require('crypto');consturl=require('url');constserver_port=8080;constserver_iterations=400000;constfibo_initialize_a1="I don't like secrets.";constfibo_initialize_a2="God is my co-pilot.";functioncombine(a1,a2){returncrypto.createHmac('sha256',a1).update(a2).digest('hex');}functionfiboLoop(n,k,a1,a2,callback){varcollaborate_p=(k%20===0);if(n==k){callback(a2);}else{if(collaborate_p){setTimeout(()=>{fiboLoop(n,k+1,a2,combine(a1,a2),callback);},1);}else{fiboLoop(n,k+1,a2,combine(a1,a2),callback);}}}functionfibo(n,callback){fiboLoop(n,0,fibo_initialize_a1,fibo_initialize_a2,callback);}functionblockingFibo(n){vara1=fibo_initialize_a1,a2=fibo_initialize_a2,tmp;for(vari=1;i<=n;i++){tmp=a2a2=combine(a1,a2);a1=tmp;}returna2;}functionsafeParseInt(text,_defaultValue){varparsedInt=parseInt(text);vardefaultValue=_defaultValue||0;returnisNaN(parsedInt)?defaultValue:parsedInt;};http.createServer(function(req,res){varparts=url.parse(req.url,true);varquery=parts.query;variterations=('n'inquery)?safeParseInt(query['n']):server_iterations;console.log('Receive fibo '+iterations);if('blocking'inquery){varf=blockingFibo(iterations);res.writeHead(200,{'Content-Type':'text/plain'});res.write('Hello World! '+f);res.end();console.log('Done blocking fibo '+iterations);}else{fibo(iterations,(ax)=>{res.writeHead(200,{'Content-Type':'text/plain'});res.write('Hello World! '+ax);res.end();console.log('Done fibo '+iterations);});}}).listen(server_port);
On se rend compte que la version coopérative est beaucoup plus lente :
$ time curl 'http://localhost:8080?n=100000'
Hello World! b04e954ac754919c16edf386311af04c0a41ef30f99c2239e479c42941572397curl 'http://localhost:8080?n=100000'0,01s user 0,00s system 0% cpu 6,727 total
$ time curl 'http://localhost:8080?n=100000&blocking'
Hello World! b04e954ac754919c16edf386311af04c0a41ef30f99c2239e479c42941572397curl 'http://localhost:8080?n=100000&blocking'0,01s user 0,00s system 2% cpu 0,411 total
Les utilisateurs ne seront pas très contents s'ils doivent attendre 7 secondes à chaque fois qu'ils s'authentifient. Et il y a d'autres solutions, comme déporter ces calculs dans d'autres processus. Mon expérience de projets nodejs en production montre que ce n'est pas évident de trouver les parties de code qui vont poser problème (surtout qu'ils se situent en général pas dans le code que l'on écrit soi-même, mais souvent dans une des centaines de bibliothèques que l'on importe via npm) et, qu'en pratique, la latence n'est pas géniale.
C'est sympa cette distinction. En pratique ça se passe comment? Il faut annoter ses routines ou bien le compilateur s'en sort tout seul? Aussi c'est un peu bizarre de penser qu'une fonction change de catégorie si on la "pepper-printf" pour déboguer.
Ça se passe comme en nodejs : le développeur n'a pas besoin d'annoter ça, le runtime du langage va faire en sorte que les calculs se passent dans le thread principal (pour nodejs) ou les threads principaux (pour go), et quand il y a une opération bloquant d'I/O, on passe ce contexte d'exécution sur un thread dédié aux I/O. Et quand l'opération d'I/O est finie, le contexte d'exécution est de nouveau dans un état où le scheduler peut l'exécuter sur un des threads principaux. La grosse différence, c'est qu'un processus nodejs n'utilise qu'un seul cœur (en gros) alors qu'un processus go peut utiliser tous les cœurs.
Si on cherche à faire facilement du redimensionnement de capacité, la bonne stratégie dépend du contexte.
Oui, bien sûr, quand on fait des projets un peu conséquents, avec des besoins de performances ou de haute disponibilité, le langage est moins importante que l'architecture globale. Mais le fait qu'un processus nodejs ne sache utiliser qu'un seul cœur reste une bonne épine dans le pied quand on conçoit ces architectures.
[^] # Re: Petit joueur
Posté par Bruno Michel (site web personnel) . En réponse au journal Des vieilles bases d'unix à la hype reactive actuelle. Évalué à 4.
Oui, mais pourtant, je l'ai déjà vu plusieurs fois sur des projets nodejs en production. Ce n'est pas toujours facile de voir à l'avance où ça va se produire. Et même quand on le sait, ce n'est pas facile à transformer. Ça demande d'une part de modifier du code synchrone en code asynchrone (normalement, ce n'est pas très compliqué, c'est juste du travail de bucheron) et, d'autre part, il faut réussir à introduire le découpage qui va bien (là, c'est plus compliqué).
Si on prend ton exemple, et que l'on ajoute la version bloquante pour comparer :
On se rend compte que la version coopérative est beaucoup plus lente :
Les utilisateurs ne seront pas très contents s'ils doivent attendre 7 secondes à chaque fois qu'ils s'authentifient. Et il y a d'autres solutions, comme déporter ces calculs dans d'autres processus. Mon expérience de projets nodejs en production montre que ce n'est pas évident de trouver les parties de code qui vont poser problème (surtout qu'ils se situent en général pas dans le code que l'on écrit soi-même, mais souvent dans une des centaines de bibliothèques que l'on importe via npm) et, qu'en pratique, la latence n'est pas géniale.
Ça se passe comme en nodejs : le développeur n'a pas besoin d'annoter ça, le runtime du langage va faire en sorte que les calculs se passent dans le thread principal (pour nodejs) ou les threads principaux (pour go), et quand il y a une opération bloquant d'I/O, on passe ce contexte d'exécution sur un thread dédié aux I/O. Et quand l'opération d'I/O est finie, le contexte d'exécution est de nouveau dans un état où le scheduler peut l'exécuter sur un des threads principaux. La grosse différence, c'est qu'un processus nodejs n'utilise qu'un seul cœur (en gros) alors qu'un processus go peut utiliser tous les cœurs.
Oui, bien sûr, quand on fait des projets un peu conséquents, avec des besoins de performances ou de haute disponibilité, le langage est moins importante que l'architecture globale. Mais le fait qu'un processus nodejs ne sache utiliser qu'un seul cœur reste une bonne épine dans le pied quand on conçoit ces architectures.