Quelle est la différence entre “bloquer” et “en cours d’exécution” ?


On vous dit qu’il faut faire attention en utilisant des technologies non bloquantes, car si on bloque dans la boucle d’événement, on bloque tout le programme, et on perd l’intérêt de l’outil.

C’est vrai, mais que veut dire “bloquer” ?

Car si je fais :

for x in range(1000000):
    print(x)

Mon programme va tourner longtemps, et la boucle d’événement va bloquer, n’est-ce pas ?

En fait, “bloquer” est un abus de langage car il y a plusieurs raisons pour bloquer. Dans notre contexte, il faudrait dire “bloquer en attente d’une entrée ou d’une sortie”. D’où l’appellation “Aynschronous non blocking I/O” des technos types NodeJS, Twisted, Tornado, Gevent, etc.

En effet, il faut distinguer deux causes d’attente à votre programme :

  • Attendre que vos instructions se terminent. C’est être “en cours d’exécution”.
  • Attendre qu’un événement extérieur (écrire sur le disque, lire une socket, un clic de souris) arrive à sa conclusion. C’est bloquer sur de l’I/O.

Le premier cas est impossible à éviter. Tout au mieux pouvons-nous répartir la charge du programme sur plusieurs cœurs, processeurs voire machines. Le code devra toujours attendre qu’il se termine, mais ça ira plus vite.

Dans le contexte de la programmation non bloquante telle qu’on vous en a parlé, on est donc dans le deuxième cas.

Il ne s’agit alors pas de s’interdire de faire des boucles ou autre opération longue (ou plutôt, c’est un problème d’optimisation ordinaire qui n’a rien à voir avec le fait de bloquer), il s’agit de ne pas “attendre à ne rien faire” quand une opération extérieure est en cours.

C’est ce que font naturellement NodeJS, Twisted, Tornado, Gevent & Co. Quand on fait un échange HTTP, le bout de données part, puis le reste du code continue de tourner, passant à la tâche suivante, en attendant que le paquet traverse le réseau, atteigne l’autre machine, qui vous répond finalement. C’est ce temps, incompressible, sans contrôle de votre côté, durant lequel il ne faut pas bloquer. Le gain de perf est que votre programme ne se la touche pas pendant les temps d’attente, mais bien entendu que VOTRE, lui, code va prendre du temps et “bloquer” le processeur. Il faut bien qu’il s’exécute.

Ce qu’on entend donc par “il ne faut pas faire d’opération bloquante dans un code qui est déjà non bloquant” c’est “il ne faut pas utiliser un outil à l’API bloquante au milieu d’autres outils non bloquants”.

Par exemple, n’utilisez pas requests avec Twisted, car requests est codé pour attendre sans rien faire jusqu’à obtenir une réponse à chaque requête, bloquant Twisted. Utilisez plutôt treq. C’est pareil pour la lecture d’un fichier, une requête de base de données, etc. Et il existe des boucles d’événements ailleurs que sur le serveur : une page Web possède sa propre boucle (c’est pour cela que tout JS est asynchrone), un toolkit GUI comme QT ou GTK aussi (c’est pour ça qu’ils utilisent la programmation événementielle), etc.

Maintenant vous allez me dire : mais pourquoi bloquer alors ? Pourquoi ne pas toujours éviter de bloquer ?

Et bien parce que si on ne bloque pas, on ne peut pas écrire un programme ligne à ligne. On est obligé d’adopter un style de programmation asynchrone puisqu’on ne sait pas quand le résultat de certaines lignes va arriver. Ça veut dire des callbacks, ou des futures, ou des coroutines, ou du message passing… Bref, un truc plus compliqué. Or, on n’a pas forcément besoin de ce niveau de performance. En fait, la grande majorité des programmes n’ont pas besoin de ce niveau de performance. Donc, on bloque en attendant, non pas Godot, mais l’I/O, parce que c’est plus simple à écrire. Pour pas se faire chier.

Il y a bien des moyens de contourner ce problème : les threads, le multiprocessing, les coroutines, etc. Parfois même, on ignore le problème : bloquer quelques ms au milieu d’une boucle d’événements une fois par seconde n’est pas un drame. Une fois que j’ai fini le dossier sur les tests unitaires, je vous ferai un dossier sur la programmation non bloquante, avec aussi une esquisse de la parallélisation.

En attendant, ne stressez pas parce que votre code “bloque” parce qu’il travaille longtemps, assurez-vous juste que les APIs que vous utilisez ne bloquent pas pendant l’I/O, et vous êtes ok.

Et comment savoir ? Et bien si une donnée rentre ou sort de votre programme (ça ne fait pas partie du code source), c’est de l’I/O. Si votre code ressemble à ça :

res = faire_operation_sur_IO()
faire_un_truc_avec_le_res(res)

Alors votre outil est bloquant, puisque qu’il compte sur le fait que la deuxième ligne sera exécutée à coup sûr quand la première sera terminée. Un outil non bloquant exigera quelque chose pour gérer le retour du résultat plus tard: un callback, une promesse, un yield

9 thoughts on “Quelle est la différence entre “bloquer” et “en cours d’exécution” ?

  • Sam Post author

    Non, entre “entre train de bloquer” et “en cours d’exécution”. Le programme n’est pas bloqué, il bloque, activement, et volontairement. C’est une action, pas un état, que je souhaites mettre en avant.

  • Pierre Durand

    En Go, tu peux très bien faire:

    res = faire_operation_sur_IO()
    faire_un_truc_avec_le_res(res)

    pourtant le thread qui l’exécute n’est pas bloqué!

    :)

  • Sam Post author

    Mais en Go il y a un mécanisme de coroutine et de channels signalées par le mot clé Go qui marque tout ça, donc on retombe dans ce que je disais :

    Un outil non bloquant exigera quelque chose pour gérer le retour du résultat plus tard: un callback, une promesse, un yield

    Python fait pareil avec yield.

  • Pierre Durand

    Les technologies “non bloquantes” ont 2 intérêts:
    – pouvoir facilement à partir d’un point de ton code, lancer plusieurs actions concurrentes, et les “regrouper” plus tard
    – pouvoir exécuter de nombreuses requêtes concurrentes sans bloquer un thread (qui est une ressource coûteuse en RAM à cause de la stack)

    Le soucis de NodeJS/Javascript, c’est que pour ces 2 points, il utilise des callbacks qui rendent le code illisible.

    En Go, c’est différent.
    Pour lancer des action concurrentes on utilise le mot clé “go”, et pour synchroniser on peut utiliser de nombreuses techniques comme les channel ou les waitgroup. C’est aussi acrobatique que les callback js, mais je vois mal comment on peut faire autrement.
    En revanche, le 2em point est mieux traité en Go qu’en JS.
    Dans le cas d’une app web, on peut très bien écrire un code “normal/bloquant”.
    Pas besoin de faire de goroutine/channel/waitgroup/”yield”.
    (bon OK, c’est le serveur http de GO qui crée des goroutines à la volée pour chaque requête, mais c’est masqué au développeur)
    Et le processus utilisera un nombre de thread très réduit.
    Je trouve ça plus élégant: on écrit un code “bloquant”, mais il se comporte comme du code “non bloquant”.

  • Sam Post author

    Cet article parle de la prog non bloquante en générale, et tu viens évangéliser GO, et en plus en mélangeant des tas de choses : comment ça marche, des questions de syntaxe, des contextes différents histoire de ne pas pouvoir faire une comparaison juste. Comme un troll emac viendrait sur un article à propos de l’encoding de caractères.

    Stop.

    Heureusement qu’on a pas encore restauré les tampons.

  • Alkareth

    @Sam : c’est pour quand ?

    on bloque en attendant, non pas Godot, mais l’I/O

    Merci pour ça, et bon article du reste. Vous avez un certain talent pour clarifier les choses et vulgariser, en un sens. Je suis curieux d’un futur dossier pour la parallélisation, c’est un peu mon dada en ce moment.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.