asynchrone – Sam & Max http://sametmax.com Du code, du cul Thu, 05 Sep 2019 08:22:03 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Utiliser requests de manière non bloquante facilement http://sametmax.com/utiliser-requests-de-maniere-non-bloquante-facilement/ http://sametmax.com/utiliser-requests-de-maniere-non-bloquante-facilement/#comments Wed, 21 Jan 2015 07:17:41 +0000 http://sametmax.com/?p=15797 En attendant le dossier sur la programmation non bloquante, voici une petite lib qui résout un cas d’école : faire une requête HTTP sans bloquer avec une jolie API, en pur Python.

Pour ça, on dégaine pip et installe requests-futures, un plugin pour la célèbre lib requests qui fonctionne avec Python 2 et 3 :

pip install requests-futures

requests-futures va créer pour vous une pool de workers (2 par défaut) et quand vous faites une requête, la lib vous retourne un objet future qui vous permet d’attacher un callback.

Fiou, le nombre de liens référant à d’autres articles du blog est en train d’exploser.

Exemple :

import time
from requests_futures.sessions import FuturesSession

# Cette session est notre point d'entrée, c'est elle
# qui gère nos workers. Faites help(FuturesSession)
# pour voir ses paramètres.
session = FuturesSession()

# Les URLs sur lesquelles on va faire
# nos requêtes
URLs = [
    "http://sametmax.com",
    "http://sebsauvage.net",
    "http://indexerror.net",
    "http://afpy.org",
    "http://0bin.net"
]

# Notre callback qui sera appelé quand une 
# des requêtes sera terminée. Il reçoit
# l'objet future pour seul paramètre
def faire_un_truc_avec_le_resultat(future):
    # On est juste intéressé par le résutlat, qui
    # est un objet response typique de la lib
    # request
    response = future.result()
    print(response.url, response.status_code)

# On traite chaque URL. Comme on a 2 workers,
# on pourra traiter au mieux 2 URLs en parallèle,
# mais toujours sans bloquer le programme
# principal
for url in URLs:
    # On fait notre requête GET
    future = session.get(url)
    # On rajoute le callback à appeler quand
    # le résultat de la requête arrive.
    # La flemme de faire la gestion des erreurs.
    future.add_done_callback(faire_un_truc_avec_le_resultat)

# Juste pour montrer que c'est bien non bloquant
for x in range(10):
    print(x)
    time.sleep(1)

Output :

0
1
(u'http://sebsauvage.net/', 200)
(u'http://sametmax.com/', 200)
2
(u'http://indexerror.net/', 200)
(u'http://0bin.net/', 200)
(u'http://www.afpy.org/', 200)
3
4
5
6
7
8
9

On remerciera turgon37 pour sa question sur IndexError qui m’a amené à écrire cet article.

]]>
http://sametmax.com/utiliser-requests-de-maniere-non-bloquante-facilement/feed/ 12 15797
Quelle est la différence entre “bloquer” et “en cours d’exécution” ? http://sametmax.com/quelle-est-la-difference-entre-bloquer-et-en-cours-dexecution/ http://sametmax.com/quelle-est-la-difference-entre-bloquer-et-en-cours-dexecution/#comments Tue, 09 Dec 2014 16:57:06 +0000 http://sametmax.com/?p=12766 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

]]>
http://sametmax.com/quelle-est-la-difference-entre-bloquer-et-en-cours-dexecution/feed/ 9 12766
Deferred, Future et Promise : le pourquoi, le comment, et quand est-ce qu’on mange http://sametmax.com/deferred-future-et-promise-le-pourquoi-le-comment-et-quand-est-ce-quon-mange/ http://sametmax.com/deferred-future-et-promise-le-pourquoi-le-comment-et-quand-est-ce-quon-mange/#comments Wed, 04 Jun 2014 13:19:22 +0000 http://sametmax.com/?p=10418 Si vous avez plongé dans le monde de la programmation asynchrone non bloquante, vous avez du vous heurter aux callbacks. Si ce n’est pas le cas, aller lire l’article, et faites vos armes sur jQuery, je vais m’en servir en exemple.

Signalement de rigueur que l’article est long :

Un callback, ça va.

Deux callbacks, pour un seul appel, ça commence à être chiant, mais c’est compréhensible.

Quand les callbacks appellent eux aussi des callbacks, ça donne des codes imbitables :

$(function(){
  $.post('/auth/token', function(token){
    saveToken(token);
    $.get('/sessions/last', function(session){
      if (session.device != currentDevice){
        $.get('/session/ ' + session.id + '/context', function(context){
          loadContext(function(){
            startApp(function(){
              initUi()
            })
          })}
        )}
      else {
        startApp(function(){
          initUi()
        })
      }}
    )
  })
});

Il y a pire que de lire ce code : le modifier ! Retirez un bloc, pour voir. Oh, et histoire de vous faire partager l’expérience complète, j’ai volontairement déplacé l’indentation d’une parenthèse et de deux brackets.

Or les codes asynchrones ont besoin de callback afin d’enchainer certaines opérations dans le bon ordre, sinon on ne peut pas récupérer le résultat d’une fonction et l’utiliser dans une autre, puisqu’on ne sait pas quand l’opération se termine.

Dans notre exemple, $.post et $.get font des requêtes POST et GET, et comme on ne sait pas quand le serveur va répondre, il faut mettre un callback pour gérer la réponse quand elle arrive. C’est plus performant que de bloquer jusqu’à ce que la première requête soit terminée car pendant ce temps, notre programme peut faire autre chose. Mais c’est aussi super relou à écrire et comprendre.

Entrent en jeu les promesses (promises). Ou les deferred. Ou les futures.

Typiquement, on retrouve des deferreds dans Twisted, des promises pour l’AJAX avec jQuery, des futures pour asyncio… Mais il y en a un peu partout de nos jours, et une lib peut utiliser plusieurs de ces concepts.

En fait c’est la même chose, un nom différent donné au même concept, par des gens qui l’ont réinventé dans leur coin. Les puristes vous diront qu’il y a des différences dans l’implémentation, ou alors que la promesse est l’interface tandis que le deferred est l’objet retourné, bla, bla, bla.

Fuck it, on va considérer que c’est tout pareil.

Les promesses sont une des manières de rendre un code asynchrone plus facile à gérer. On dit : ce groupe de fonctions doit s’exécuter dans un ordre car elles sont dépendantes les unes des autres.

Il y a d’autres moyens de gérer le problème de l’asynchrone: des événements, des queues, etc. L’avantage des promesses c’est que c’est assez simple, et ça marche là où on utilisait des callbacks avant, donc on a pu les rajouter aux libs qui étaient blindées de callbacks.

Le principe

La promesse est un moyen de dire que certaines fonctions, bien que non bloquantes et asynchrones, sont liées entre elles, et doivent s’exécuter les unes à la suite des autres. Cela permet de donner un ordre d’exécution à un groupe de fonctions, et surtout, que chaque fonction puisse accéder au résultat de la fonction précédente. Tout ceci sans bloquer le reste du système asynchrone.

En résumé, cela donne un gout de programmation synchrone, à quelque chose qui ne l’est pas.

Cela se passe ainsi :

  • La fonction asynchrone retourne un objet immédiatement : la promesse.
  • On ne passe pas de callback à la fonction. On rajoute un callback à la promesse.
  • Le callback prend en paramètre le résultat de la fonction asynchrone.
  • Le callback retourne le résultat de son traitement.
  • On peut rajouter autant de callbacks qu’on veut à la promesse, chacun devant accepter le résultat du callback précédent et retourner son propre résultat.
  • Si un des callbacks retourne une promesse, elle est fusionnée avec la promesse initiale, et c’est son résultat que le prochain callback va récupérer

Voilà un exemple :

// $.get est asynchrone. On a pas le résultat tout de suite, mais en attendant
// on a une promesse tout de suite.
var $promesse = $.get('/truc/machin');

// premier callback. Il sera appelé quand $.get aura récupéré son
// résultat
$promesse.then(function(resultat){
  // faire un truc avec le résultat
  // puis on retourne le nouveau résultat
  return nouveau_resultat;
});

// deuxième callback. Il sera appelé quand le premier callback
// aura retourné son résultat.
$promesse.then(function(nouveau_resultat){
  // faire un truc
});

Notez bien que c’est TRES différent de ça (en Python):

resultat = request.get('/truc/marchin')

def function(resultat):
  # faire un truc
  return nouveau_resultat
nouveau_resultat = function(resultat)

def autre_function(nouveau_resultat):
  # faire un truc
autre_function(nouveau_resultat)

En Python, le code est bloquant par défaut. Ça va marcher, mais pendant que le code attend la réponse du serveur, votre ordinateur est en pause et ne travaille pas.

Un plus beau code

On se retrouve avec un code asynchrone, mais qui s’exécute dans l’ordre de lecture. Et comme on peut chainer les then() et donc ne pas réécrire $promesse à chaque fois, on obtient quelque chose de beaucoup plus lisible :

$.get('/truc/machin')
.then(function(resultat){
  // faire un truc
  return nouveau_resultat;
})
.then(function(nouveau_resultat){
  // faire un truc
});

Si on reprend notre premier exemple, ça donne ça :

$(function(){

// create new token
$.post('/auth/token')

// then save token and get last session
.then(function(token){
  saveToken(token);
  return $.get('/sessions/last');
})

// then init session
.then(function(session){
  if (session.device != currentDevice){
    
    $.get('/session/ ' + session.id + '/context')
    .then(function(context){
      loadContext(function(){
        startApp(function(){
          initUi()
        })
      })
    })

  }
  else {
    startApp(function(){
      initUi()
    })
  }}
})

});

Tout ça s’exécute de manière non bloquante (d’autres fonctions ailleurs dans le programme peuvent s’exécuter pendant qu’on attend la réponse du serveur), mais dans l’ordre de lecture, donc on comprend bien ce qui se passe. Si on veut retirer un bloc, c’est beaucoup plus facile.

Comment ça marche à l’intérieur ?

Histoire d’avoir une idée de comment une promise marche, on va faire une implémentation, simpliste et naïve, mais compréhensible, d’une promesse en Python. Pour rendre l’API un peu sympa,je vais utiliser les décorateurs.

class Promise:

    # La promesse contient une liste de callbacks, donc une liste de fonctions.
    # Pas le résultat des fonctions, mais bien les fonctions elles mêmes,
    # puisque les fonctions sont manipulables en Python.
    def __init__(self):
        self.callbacks = []

    # Point d'entrée pour ajouter un callback à la promesse
    def then(self, callback):
        self.callbacks.append(callback)

    # Cette méthode est celle qui sera appelée par le code asynchrone
    # quand il reçoit son résultat.
    def resolve(self, resultat):

        # Ici, on obtient le résultat du code asycnhrone, donc on boucle
        # sur les callbacks pour les appeler
        while self.callbacks:
            # On retire le premier callback de la liste, et on l'appelle
            # avec le résultat
            resultat = self.callbacks.pop(0)(resultat)

            # Si le resultat est une promesse, on dit à cette nouvelle promesse
            # de nous rappeler quand elle a reçu ses résultats à elle avant
            # d'aller le reste de nos callbacks à nous : on fusionne les deux
            # promesses :
            # Promesse 1
            #  - callback1
            #  - callback2
            #  - Promesse 2
            #      * callback 1
            #      * callback 2
            #  - callback 3
            if isinstance(resultat, Promise):
                resultat.then(self.resolve)
                break

Maintenant, créons un code asynchrone:

from threading import Timer

def func1(v1):
    # On dit complètement artificiellement d'afficher le résultat
    # de la fonction dans 3 secondes, sans bloquer histoire d'avoir
    # un peu de nonbloquitude dans notre code et justifier l'asynchrone.
    def callback1():
        print(v1)
    t = Timer(3, callback1)
    t.start()

def func2(v2):
    # Le même, mais pour 2 secondes
    def callback2():
        print(v2)
    t = Timer(2, callback2)
    t.start()

# Deux fonctions normales
def func3(v3):
    print(v3)

def func4(v4):
    print(v4)

# Et si on les enchaines...
print('Je commence')
func1(1)
print('Juste après')
func2(2)
func3(3)
func4(4)

# ... le résultat est bien désordonné :

## Je commence
## Juste après
## 3
## 4
## 2
## 1

Parfois c’est ce que l’on veut, que les choses s’exécutent dans le désordre, sans bloquer.

Mais quand on a des fonctions qui dépendent les unes des autres, au milieu d’un code asynchrone, on veut qu’elles se transmettent le résultat les unes aux autres au bon moment. Pour cela, utilisons notre promesse :

from threading import Timer


# La mise en place de promesses suppose que le code 
# écrit en fasse explicitement usage. Notre code est
# définitivement lié à cette manière de faire.

def func1(v1):
    # Notre fonction doit créer la promesse et la retourner
    p = Promise()
    def callback1():
        print(v1)
        # Dans le callback, elle doit dire quand la promesse est tenue
        p.resolve(v1)
    t = Timer(3, callback1)
    t.start()
    return p

# On lance la première fonction.
print('Je commence')
promise = func1(1)
print('Juste après')

# On ajoute des callbacks à notre promesse.

@promise.then
def func2(v2):
    p = Promise()
    def callback2():
        # Pour justifier l’enchainement des fonctions, on fait en sorte que
        # chaque fonction attend le résultat de la précédente, et
        # l'incrémente de 1.
        print(v2 + 1)
        p.resolve(v2 + 1)
    t = Timer(2, callback2)
    t.start()
    # Ce callback retourne lui-même une promesse, qui sera fusionnée
    return p

# Ces callbacks ne retournent pas de promesses, et seront chainés
# normalement
@promise.then
def func3(v3):
    print(v3 + 1)
    return v3 + 1

@promise.then
def func4(v4):
    print(v4 + 1)

# Nos fonctions s'exécutent dans le bon ordre, mais bien de manière
# asynchrone par rapport au reste du programme.

## Je commence
## Juste après
## 1
## 2
## 3
## 4

Notez bien :

  • Le résultat “1” n’apparait que trois secondes après “Juste après”. Les fonctions sont donc bien non bloquantes.
  • Le resultat “2” apparait deux secondes après “1”: c’est aussi asynchrone, MAIS, n’est lancé que quand la première fonction a terminé son travail.
  • La deuxième fonction retourne une promesse, qui est fusionnée: tous ses callbacks vont s’exécuter en file avant que func3 soit lancé.

Évidement, n’utilisez pas cette implémentation de promise à la maison, c’est pédagogique. Ça ne gère pas les erreurs, ni le cas où le callback est enregistré après l’arrivée du résultat, et tout un tas d’autres cas tordus.

Syntaxe alternative

En Python, beaucoup de frameworks ont une approche plus agréable pour gérer les promesses à grand coup de yield. Twisted fait ça avec son @inlineCallback, asyncio avec @coroutine. C’est juste du sucre syntaxique pour vous rendre la vie plus facile.

Il s’agit de transformer une fonction en générateur, et à chaque fois qu’on appelle yield sur une promesse, elle est fusionnée avec la précédente. Ça donne presque l’impression d’écrire un code bloquant normal :

# un appel de fonction asyncrone typique de twisted
@inlineCallback
def une_fonction(data):
  data = yield func1(data)
  data = yield func2(data)
  data = yield func3(data)

une_fonction(truc)

Les fonctions 1, 2 et 3 vont ainsi être appelées de manière asynchrone par rapport au reste du programme, mais bien s’enchainer les unes à la suite des autres.

Ouai, tout ce bordel parce que l’asynchrone, c’est dur, donc on essaye de le faire ressembler à du code synchrone, qui lui est facile.

]]>
http://sametmax.com/deferred-future-et-promise-le-pourquoi-le-comment-et-quand-est-ce-quon-mange/feed/ 19 10418
En attendant asyncio http://sametmax.com/en-attendant-asyncio/ http://sametmax.com/en-attendant-asyncio/#comments Fri, 17 Jan 2014 14:09:59 +0000 http://sametmax.com/?p=8781 La programmation asynchrone arrive en force avec la version 3.4, mais celle-ci n’est pas encore en version stable. En attendant, Python 3 possède déjà de quoi faire de la programmation asynchrone, et même parallèle, avec une bien plus grande facilité qu’en Python 2.

Si vous avez oublié le principe ou l’intérêt de la programmation asynchrone, il y a un article pour ça ©.

Pour montrer l’intérêt de la chose, nous allons utiliser un bout de code pour télécharger le code HTML de pages Web.

Sans programmation asynchrone

Le code est simple et sans chichi :

# -*- coding: utf-8 -*-

import datetime
from urllib.request import urlopen

start_time = datetime.datetime.now()

URLS = ['http://sebsauvage.net/',
        'http://github.com/',
        'http://sametmax.com/',
        'http://duckduckgo.com/',
        'http://0bin.net/',
        'http://bitstamp.net/']

for url in URLS:
    try:
        # j'ignore volontairement toute gestion d'erreur évoluée
        result = urlopen(url).read()
        print('%s page: %s bytes' % (url, len(result)))
    except Exception as e:
        print('%s generated an exception: %s' % (url, e))

elsapsed_time = datetime.datetime.now() - start_time

print("Elapsed time: %ss" % elsapsed_time.total_seconds())

Ce qui nous donne:

python sans_future.py
http://sebsauvage.net/ page: 9036 bytes
http://github.com/ page: 12582 bytes
http://sametmax.com/ generated an exception: HTTP Error 502: Bad Gateway
http://duckduckgo.com/ page: 8826 bytes
http://0bin.net/ page: 5551 bytes
http://bitstamp.net/ page: 51996 bytes
Elapsed time: 25.536095s

Erreur 500 sur S&M… Mon script qui se fout de ma gueule en plus…

Avec programmation asynchrone

On utilise le module future, qui, comme sont nom l’indique, implémente des outils pour manipuler des “futures” en Python. Il inclut notamment un context manager pour créer, lancer et arrêter des workers automatiquement, et leur envoyer des tâches, puis récupérer les résultats de ces tâches sous forme de “futures”.

Pour rappel, une “future” est juste un objet qui représente le résultat d’une opération asynchrone (puisqu’on ne sait pas quand elle se termine). Cet objet contient des méthodes pour vérifier si le résultat est disponible à un instant t, et obtenir ce résultat si c’est le cas.

# -*- coding: utf-8 -*-

import datetime
import concurrent.futures

from urllib.request import urlopen
from concurrent.futures import ProcessPoolExecutor, as_completed

start_time = datetime.datetime.now()

URLS = ['http://sebsauvage.net/',
        'http://github.com/',
        'http://sametmax.com/',
        'http://duckduckgo.com/',
        'http://0bin.net/',
        'http://bitstamp.net/']


def load_url(url):
    """
        Le callback que vont appeler les workers pour télécharger le contenu
        d'un site. On peut appeler cela une 'tâche'
    """
    return urlopen(url).read()

# Un pool executor est un context manager qui va automatiquement créer des
# processus Python séparés et répartir les tâches qu'on va lui envoyer entre
# ces processus (appelés workers, ici on en utilise 5).
with ProcessPoolExecutor(max_workers=5) as e:

    # On e.submit() envoie les tâches à l'executor qui les dispatch aux
    # workers. Ces derniers appelleront "load_url(url)". "e.submit()" retourne
    # une structure de données appelées "future", qui représente  un accès au
    # résultat asynchrone, qu'il soit résolu ou non.
    futures_and_url = {e.submit(load_url, url): url for url in URLS}

    # "as_completed()" prend un iterable de future, et retourne un générateur
    # qui itère sur les futures au fur et à mesures que celles
    # ci sont résolues. Les premiers résultats sont donc les premiers arrivés,
    # donc on récupère le contenu des sites qui ont été les premiers à répondre
    # en premier, et non dans l'ordre des URLS.
    for future in as_completed(futures_and_url):

        # Une future est hashable, et peut donc être une clé de dictionnaire.
        # On s'en sert ici pour récupérer l'URL correspondant à cette future.
        url = futures_and_url[future]

        # On affiche le résultats contenu des sites si les futures le contienne.
        # Si elles contiennent une exception, on affiche l'exception.
        if future.exception() is not None:
            print('%s generated an exception: %s' % (url, future.exception()))
        else:
            print('%s page: %s bytes' % (url, len(future.result())))


elsapsed_time = datetime.datetime.now() - start_time

print("Elapsed time: %ss" % elsapsed_time.total_seconds())

Et c’est quand même vachement plus rapide :

python3 avec_future.py # notez qu'on utilise Python 3 cette fois
http://duckduckgo.com/ page: 8826 bytes
http://sebsauvage.net/ page: 9036 bytes
http://github.com/ page: 12582 bytes
http://sametmax.com/ page: 50998 bytes
http://0bin.net/ page: 5551 bytes
http://bitstamp.net/ page: 52001 bytes
Elapsed time: 3.480596s

Même si vous retirez les commentaires, le code est encore très verbeux, ce qui explique pourquoi j’attends avec impatience asyncio qui, grâce à yield from, va intégrer l’asynchrone de manière plus naturelle au langage.

Mais ça reste beaucoup plus simple que de créer son process à la main, créer une queue, envoyer les tâches dans la queue, s’assurer que le process est arrêté, gérer les erreurs et le clean up, etc.

Notez qu’on peut remplacer ProcessPoolExecutor par ThreadPoolExecutor si vous n’avez pas besoin d’un process séparé mais juste de l’IO non bloquant.


Télécharger le code de larticle : avec future / sans future.

]]>
http://sametmax.com/en-attendant-asyncio/feed/ 15 8781
La différence entre la programmation asynchrone, parallèle et concurrente http://sametmax.com/la-difference-entre-la-programmation-asynchrone-parallele-et-concurrente/ http://sametmax.com/la-difference-entre-la-programmation-asynchrone-parallele-et-concurrente/#comments Wed, 09 Oct 2013 22:08:13 +0000 http://sametmax.com/?p=7378 On parle un peu partout de programmation non bloquante ces temps-ci. NoSQL a remis le map/reduce au goût du jour, et PAF, on vous sort le mot clé parallélisation pour vous en vendre une tetrachiée. Les partisants de NodeJS vont crier “asynchrone”, parce que c’est ce que Javascript sait faire de mieux. Et on murmure dans les coins que la robustesse d’Erlang tient dans ses acteurs qui travaillent de manière concurrente dans la VM.

Ok, donc tout ça, ça à l’air de faire la même chose, c’est à dire de faire plusieurs choses en même temps, sans bloquer.

Donc c’est pareil ?

Non. En fait c’est une question de point de vue : non bloquant dans quel contexte ?

Si c’est l’IO, c’est asynchrone

Pour rappel, l’IO (Input/Ouput), c’est toute activité qui implique que des données entrent et sortent de votre programme : saisie utilisateur, print sur un terminal, lecture sur une socket, écriture sur le disque, etc. Une opération I/O a plusieurs caractéristiques :

  • Le temps que prend l’opération n’est pas dépendant du CPU : la vitesse du disque, la latence du réseau, le nombre d’heures de sommeil du sysadmin sont les facteurs qui vont déterminer quand l’opération va prendre fin.
  • Le corollaire, c’est qu’on ne peut pas prédire quand l’opération va prendre fin depuis le programme.
  • Sur les services avec beaucoup d’I/O (serveurs Web, bases de données, crawlers, scripts de déploiement, etc), c’est l’I/O qui généralement prend le plus de temps dans l’exécution du programme. L’optimisation de ces opérations va donc l’accélérer bien plus que de changer votre algo.

La plupart des programmes bloquent quand ils effectuent une opération I/O. Par exemple, si vous faites ceci en Python :

import urllib2

# télécharge et affiche le contenu de la page d'acceuil de sam et max
print(urllib2.urlopen('http://sametmax.com').read())
print("Coucou")

La ligne print("Coucou") ne s’exécutera pas tant que la ligne précédente n’aura pas terminé de s’exécuter. Dans ce cas ce n’est pas très grâve, mais dans ce cas là :

import urllib2


mille_urls = obtenir_liste_de_mille_urls()
contenu = []

# télécharge et sauvegarde dans une liste
# le contenu de chacune des 1000 urls
for url in mille_urls:
    contenu.append(urllib2.urlopen(url).read())

Chaque url est téléchargée une par une, et comme Internet, c’est vachement lent (300 ms X 1000, ça fait 5 minutes, mine de rien), votre programme va prendre un temps fou. Et pour rien en plus, car votre programme va passer la majeure partie du temps à ne rien faire ! En effet, 99% du temps de votre programme est passé à attendre qu’Internet réponde, pendant que votre CPU se touche les noix.

La programmation asynchrone est une réponse à cela : au lieu d’attendre que se finissent les entrées et les sorties, le programme continue de fonctionner.

Une autre problématique se pose alors : comment obtenir le résultat de l’opération d’I/O, puisqu’on ne sait pas quand il va arriver et qu’on attend pas qu’il arrive ?

C’est là que les systèmes asynchrones font un peu de magie. En vérité, une partie du programme attend, mais discrètement, en arrière plan, au niveau de ce qu’on appelle une boucle d’événements (“events loop”), c’est à dire une boucle infinie qui check régulièrement si une opération I/O ne s’est pas terminée.

Cette boucle est invisible pour vous, votre programme continue de tourner. Mais si une opération I/O envoie des données, alors l’events loop va réagir.

Ca a l’air compliqué, mais en fait, c’est, la plupart du temps, juste une histoire de callback (si la notion vous échappe, je vous renvois à l’article dédié…). Par exemple en Javascript :

var mille_urls = obtenir_liste_de_mille_urls();
var contenu = [];

# notre callback qui va permettre d'ajouter 
# le contenu téléchargé à notre liste
var callback = function(data) { 
      contenu.push(data);
};

# Bon, j'utilise jquery pour simplifier le code...
# On boucle sur les milles URL
$.each(mille_urls, function(index, url) {
  # On télécharge le contenu, MAIS comme
  # $.get est naturellement non blocante,
  # elle ne va pas attendre qu'internet 
  # réponde pour continuer la boucle, et
  # donc on pourra attendre plusieurs réponses
  # en même temps. Pour avoir le résultat de 
  # chaque réponse, on passe un callback qui 
  # va être appelé quand la réponse arrive.
  $.get(url, callback);

});

Comprenez bien la subtilité : à tout moment, il n’y a qu’UN SEUL process javascript qui s’éxécute. Il n’y a pas deux traitements, pas de threads, pas de processus parallèles, rien de tout ça. Simplement, Javascript n’attend pas la réponse de sa requête pour faire la requête suivante, il continu sur sa lancée, et donc peut optimiser les temps d’attente en attendant plusieurs choses en même temps.

Javascript utilise massivement des API asynchrones, c’est au cœur du langage, il n’y a aucun effort à faire pour cela. A l’inverse, Python est synchrone par nature, et il faut vraiment se faire chier pour obtenir un algo asynchrone. Ceci changera avec Python 3.4 qui accueillera tulip dans la stdlib, afin de se moderniser sur ce point. En attendant, si vous voulez faire de l’asynchrone en Python, vous pouvez voir du côté de gevent, monocle ou Tornado. L’alternative est d’utiliser des threads ou des processus séparés, ce qui ne demande rien à installer, mais est un peu verbeux, et est moins performant.

Souvenez-vous que l’I/O, c’est toute entrée et sortie du programme. Un clic sur un bouton, c’est une entrée, mettre à jour un élément du DOM dans le navigateur, c’est une sortie. La programmation asynchrone est donc importante pour la réactivité des programmes.

Si un algorithme peut répartir son travail en plusieurs bouts, c’est parallèle

Par exemple, vous avez 1000 images en haute définition à traiter : il faut les redimensionner, les mettre en noir et blanc et ajouter une ombre sur les bords. Là, la partie de votre programme qui prend le plus de temps, c’est le traitement des images, pas l’I/O, et donc c’est le CPU. Par exemple, en Python :

for image in obtenir_liste_images():
    # I/O
    data = lire_image(image) 

    # gros du travail
    redimensioner(data)
    mettre_en_noir_et_blanc(data)
    ajouter_ombre(data)

    # I/O
    ecrire_image(data, image)

Si vous avez plusieurs ordinateurs, une manière de paralléliser le travail est de mettre 500 images sur l’un, et 500 images sur l’autre, et de lancer le script sur chaque ordi.

Si vous avez plusieurs processeurs dans votre ordi (ce qui est le cas de tous les ordis modernes, et plus seulement les super-calculateurs comme il y a 10 ans), vous pouvez aussi paralléliser le travail sur une seule machine : chaque processeur va s’occuper d’une partie du taf.

Bien entendu, vous pouvez lancer le script 2 fois, mais cela ne marche que sur des travaux simples comme celui là. Et ça suppose que vous connaissez le nombre de CPU que vous voulez faire travailler à l’avance.

Une manière de faire plus propre est d’utiliser des threads ou des processus séparés. En Python, le thread ne servirait à rien, car on se heurterait au GIL, le fameux global interpréteur lock, qui fait qu’une VM n’utilise qu’un processeur, quoi qu’il arrive. Les threads ne sont donc utiles (en Python), que pour l’I/O. Par contre on peut utiliser plusieurs processus :

from multiprocessing import Process

def traiter_les_images(debut, fin):

 for image in obtenir_liste_images()[debut, fin]:
    # I/O
    data = lire_image(image) 

    # gros du travail
    redimensioner(data)
    mettre_en_noir_et_blanc(data)
    ajouter_ombre(data)

    # I/O
    ecrire_image(data, image)

# On crée deux processus, un pour traiter les 500 premières images,
# un pour traiter les images de 500 à 1000
p1 = Process(target=traiter_les_images, args=(0, 500))
p2 = Process(target=traiter_les_images, args=(500, 1000))
# On les démarre, ils se séparent alors du programme pour
# devenir indépendant
p1.start()
p2.start()
# on dit au programme d'attendre la fin des deux processus
# CE programme bloque ici, mais les deux processus, eux,
# ne bloquent pas.
p1.join()
p2.join()

Dans cet exemple, il y a TROIS processus : votre programme Python, et les deux processus qui vont traiter les photos, qui consistent ni plus ni moins en la fonction traiter_les_images() qui a maintenant un process pour elle toute seule.

La plupart des langages ont ce genre de mécanisme pour faire du travail en parallèle. Java utilise les threads par exemple. Javascript utilise les Web Workers.

Nous traitons des données de plus en plus massives (jeux vidéos, encoding divx, retouche d’images, montage de sons…), et maîtriser la parallélisation permet donc d’optimiser les ressources de nos machines modernes afin d’être toujours plus efficace.

Si il y a plusieurs entités indépendantes, c’est concurrent

Si vous avez un serveur et un client, c’est de la programmation concurrente. Si vous avez un module qui s’occupe des I/O utilisateurs, un qui s’occupe de la base de données et un qui surveille le comportement de l’OS, dans des processus séparés, et qui communiquent entre eux, c’est de la programmation concurrente.

La programmation concurrente suppose que chaque acteur de votre système est indépendant et possède son propre état. Idéalement, les acteurs sont capables de communiquer entre eux. Généralement, ils partagent une ressource à laquelle ils doivent accéder, par exemple un fichier de log. Et c’est là qu’il faut faire attention : certaines ressources ne sont pas faites pour êtres utilisées en même temps par plusieurs process. C’est pour ça qu’on parle d’accès concurrent comme d’un gros problème en informatique.

Un exemple de programmation concurrente en Python serait d’avoir un process qui regarde régulièrement si il y a des mails, et les sauvegarde. Si il reçoit un message suspect, il envoie le message à un autre process, un anti-virus, qui en plus de surveiller l’ordi, peut désinfecter le mail. Exemple :

from multiprocessing import Process, Queue

entree_traiteur_de_mail = Queue()
entree_anti_virus = Queue()

def traiter_les_mails():

    # Les processus qui tournent continuellement
    # en arrière plan sont juste boucle infinie
    while True:
        mail = obtenir_mail()
        # Si un mail est suspect, on l'envoie
        # au processus de l'anti-virus, 
        # et on attend qu'il nous le renvoie
        # tout propres.
        # Les deux processus sont indépendant,
        # ils fonctionnent l'un sans l'autre et
        # ne sont pas dans la même VM.
        if mail_est_suspect(mail):
            entree_anti_virus.put(mail)
            mail = entree_traiteur_de_mail.get()
        sauvegarder_mail(mail)


def anti_virus():

    while True:
        # L'anti-virus vérifie périodiquement 
        # s'il n'a pas un mail à nettoyer,
        # mais n'attend que 0.01 seconde, et si
        # rien ne se présente, continue son 
        # travail.
        try:
            # Si il y a un mail à désinfecter,
            # il le nettoie, et le renvoie
            # au processus de traitement de mails.
            mail = entree_anti_virus.get(0.01)
            desinfecter_mail(mail)
            entree_traiteur_de_mail.put(mail)
        except TimeoutError:
            pass
        # L'anti-virus ne fait pas que desinfecter 
        # les mails, il a d'autres tâches à lui
        verifier_virus_sur_system()


# On lance les process. La plupart du temps, il n'y a 
# pas de mail suspect, et donc les deux processus
# n'en bloquent pas. En cas de mail suspect ils bloquent
# le temps d'échanger le mail entre eux.
process_traitement_mail = Process(target=traiter_les_mails)
process_anti_virus = Process(target=anti_virus)
process_anti_virus.start()
process_traitement_mail.start()
process_anti_virus.join()
process_traitement_mail.join()

La programmation concurrente est donc une question d’architecture : vous êtes en concurrence ou non si vous décidez de répartir votre code entre plusieurs acteurs indépendant ou non. Les acteurs peuvent avoir des tâches distinctes, et ne pas se bloquer, mais communiquer sur les tâches communes. L’avantage de la programmation concurrente, c’est sa robustesse : si un process plante, le reste de votre programme continue de fonctionner. C’est pour cette raison qu’Erlang, un langage connu pour créer des systèmes increvables, base toute sa philosophie là dessus : un programme Erlang est composé de milliers d’acteurs communiquant entre eux par messages.

Hey, mais, attends là !

Ton exemple de programmation parallèle, c’est aussi une exécution concurrente. Et puis si on fait pleins de processus, pour faire la même tâche d’I/O, ils ne se bloquent pas entre eux, donc c’est non bloquant sur l’I/O, c’est asynchrone !

Allez-vous me dire, fort intelligement. Car nous avons des lecteurs intelligents.

Hé oui, effectivement, ce sont des notions qui se chevauchent. Comme je vous l’ai dit, c’est une question de point de vue. Si on se place du point de vue de l’algo, on peut paralléliser le traitement, ou non. Et il y a plusieurs manières de paralléliser. Si on se place du point de vue de l’I/O, on peut bloquer ou non, et alors on est dans de l’asynchrone. Si on se place du point de vue des acteurs, on peut en avoir plusieurs indépendants ou non, alors on est en concurrence.

En fait, même plusieurs acteurs qui communiquent entre eux sont considérés comme étant chacun en train de faire de l’I/O, avec les autres…

Bref, ces 3 termes, c’est de la sémantiques. Au final, ce qui importe, c’est que vous compreniez les enjeux qu’il y a derrière pour écrire un programme qui fasse son boulot comme il faut, et finisse en temps et en heure.

]]>
http://sametmax.com/la-difference-entre-la-programmation-asynchrone-parallele-et-concurrente/feed/ 35 7378