Instantané sur le parallélisme et le code

Posté par  (site web personnel) . Édité par palm123, BAud, Benoît Sibaud, ZeroHeure et PolePosition. Modéré par ZeroHeure. Licence CC By‑SA.
Étiquettes :
54
22
juin
2015
Technologie

Avant, la fréquence des micro-processeurs semblait suivre une loi de Moore, mais à mesure que les limites physiques ont été approchées il a fallu innover, et le multi-cœur est devenu le standard, même sur mobile.

Développer une application qui sache utiliser plusieurs fils d’exécution ou processus en même temps n'est pas un problème forcément facile, pour lequel de nombreuses méthodes ont vu le jour. Ici, on vous propose une vision haut niveau de quelques idées du passé, mais surtout d'aujourd'hui, pour utiliser la puissance de nos machines sans écrire des bogues plus vite que nos ombres.

Sommaire

Exposé du problème

Imaginez que vous deviez écrire et lire un même fichier dans deux processus différents. C'est notre cas simple de départ.

Le cas trivial

Dans ce cas, vos processus ne tournent jamais en même temps, ou alors aucun n'écrit quoi que ce soit sur le fichier et se contente de lire le fichier.
Tout devrait marcher correctement.

Le cas partagé

Vos deux processus se mettent à tourner en même temps et ils ont carte blanche pour écrire ou supprimer le fichier.

Il se peut que l'un des processus vérifie l'existence du fichier avant d'y écrire quelque chose, mais que l'autre processus l'efface juste après. Résultat : le premier processus va planter ou retourner une exception. Pourtant, le code aura l'air juste.

Différence entre concurrence et parallélisme

La concurrence est d'abord le fait de partager un bout de la mémoire avec plusieurs processus (ou fils d'exécutions).

Le parallélisme consiste en l'exécution de plusieurs processus en même temps, sur des données disjointes.

C'est une distinction qui est plus forte en anglais, puisque en français le parallélisme n'est qu'un type de concurrence. Mais la séparation des deux est pratique d'un point de vue pédagogique.

Les verrous et sémaphores

La première idée que l'on apprend consiste à mettre un cadenas sur l'espace mémoire partagé, avec une seule et même clé. C'est ce que l'on appelle un verrou. Les sémaphores ne sont que le cas où l'on fournit un nombre fini de clés supérieur à 1.

En pseudo-code, ça ressemble souvent à ça :

tant que verrou == 1:
    attend()
verrou = 1
## Je calcule ce dont j'ai besoin ici
## Et c'est assez long
## Puis je rend le verrou
verrou = 0

Avec deux processus, ça peut aller, mais quand le nombre augmente, il est très facile d'oublier de redonner la clé, et il est très facile de se retrouver avec des processus qui ne font qu'attendre une clé qui n'arrive jamais. Le nombre de cas différents à considérer augmente de manière factorielle avec le nombre de bouts de mémoires partagés.

La quasi-totalité des langages de programmation vous laisseront faire ça.

Le nombre élevé d'erreurs engendrées par cette technique a conduit à l'élaboration d'abstractions ou de manières de faire différentes.

Le modèle à acteurs (et objets actifs)

Dans les années 1970, une autre idée est apparue : au lieu de partager des zones mémoires et de bloquer de manière difficilement prévisible, pourquoi ne pas supposer qu'un processus est ce que l'on appelle un acteur, et qu'il ne peut communiquer avec d'autres acteurs que via des messages.

Chaque acteur effectue une action demandée par le biais de messages.

Erlang est le plus grand représentant de cette manière de faire. L'avantage est que la manière d'envoyer les messages permet de facilement parler à des processus sur le réseau, sans avoir rien de spécial à faire.

L'autre avantage, c'est la possibilité de pouvoir déboguer beaucoup plus facilement, même s'il est toujours possible que le système se bloque si tout le monde se met à attendre.
Enfin, si un acteur se plante, il peut simplement être relancé et n'impacte pas les autres acteurs.

En pseudo-code, ça se passerait à peu près comme ça :

# calcul_x étant une fonction quelconque, attendant 2 paramètres.
mon_acteur = nouvel_acteur()
resultat = envoyer_a(mon_acteur, calcul_x, parametre_1, parametre_2)
#
# Il est à noter que `resultat` n'est pas forcément calculé de manière synchrone
# puisque `mon_acteur` fait peut-être autre chose, mais cette nouvelle demande est dans sa liste (ou boîte mél)

Les objets actifs ne sont que de simples instances de classes, considérés comme des acteurs, ce qui s'insère très simplement dans la programmation objet : l'appel à une méthode se fait comme d'habitude, mais elle est transformée en message passé au-dit acteur à l’exécution, par exemple).

À noter : Message Passing Interface (MPI), utilisé principalement au sein des supercalculateurs est un modèle de communication entre machines et leurs processus. Le modèle à acteur est transparent (c'est-à-dire qu'un acteur peut être sur la machine locale ou sur une autre machine). D'ailleurs, MPI peut être utilisé pour implémenter le modèle à acteur.

Le modèle à acteur n'est qu'un exemple de la manière de penser de tout un pan de recherche basé sur l'algèbre de processus qui a le vent en poupe puisque c'est quelque chose de calqué sur des processus naturels dont on a l'habitude (comme les communications intercellulaires au sein d'un organisme).

Je vous conseille d'essayer ça avec Erlang, Akka ou Quasar pour Java/Scala et Celluloid pour Ruby.

Il est également possible de faire ça en Python grâce à Pykka par exemple (ou pulsar), SObjectizer ou CAF_C++ Actor Framework pour le C++, hactor pour Haskell.

Mémoire transactionnelle logicielle (Software Transactional Memory — STM)

Une autre idée est de se baser sur ce que font les bases de données, notamment en assurant l'atomicité des transactions : c'est l'idée de la mémoire transactionnelle (dans les années 80, sous le nom de Paratran)

C'est-à-dire que la transaction commence lorsque vous ouvrez le fichier, et se termine quand vous avez fini d'écrire. Rien ne peut se passer sur le fichier avant que vous n'ayez fini.
C'est une manière de cacher les mutex, mais c'est souvent implémenté sans verrous.

De manière générale, cette technique est implémentée de manière optimiste, notamment dans Haskell, avec un type de variable spéciale : la TVar.

L'idée étant que chaque processus utilisant une TVar le fait comme si ne rien n'était, à partir de la valeur de la TVar telle qu'elle était à la vérification.
Puis, chaque modification de cette TVar est enregistrée dans un journal propre à chaque processus.

En pseudo code, voici à quoi ça pourrait ressembler :

bloc atomique:
    valeur = lire_TVAR(x)
    # on fait plein de chose, et la valeur change
    ecrire_TVAR(x, valeur)
#fin bloc atomique

À la fin du bloc atomique, la valeur de la TVar est lue et comparée à celle lue en début du bloc. Si cette valeur est identique, alors personne n'a modifié la TVar, et la nouvelle valeur est écrite de manière sûre. Dans le cas contraire, le bloc atomique est rejoué avec la nouvelle valeur de la TVar, d'où le nom optimiste.

En Haskell, il est possible d'attendre une certaine valeur pour une TVar via un if et le « mot clé » retry : un signal est lié à la TVar, et sa valeur n'est re-vérifiée qu'après avoir été changée (programmation évènementielle). Tout ceci étant transparent pour le développeur. En pseudo-code, ça donne quelque chose comme ça :

bloc atomique:
    valeur = lire_TVAR(x)
    si valeur > 0:
        retry
        # tout le bloc atomique sera ré-exécuté quand `valeur` aura changé. Ce qui bloque le processus.
    ecrire_TVAR(x, nouvelle_valeur)
# fin bloc atomique

Je vous conseille d'essayer ça en Haskell avec STM, en Java avec DeuceSTM ou avec Clojure.

C'est également possible en C++ avec TinySTM, . Par contre, en Python, il faudra repasser.

Voilà

Vous avez pu entrevoir quelques techniques permettant de gérer la concurrence d'une manière moins traditionnelle, et vous êtes libres d'essayer tout ça si ça vous a plu.

Les commentaires sont également là pour préciser des choses, ou discuter d'autres systèmes qui ne sont pas mentionnés dans cette dépêche.

  • # PyPy-STM

    Posté par  . Évalué à 6.

    Par contre, en Python, il faudra repasser

    PyPy-STM est dans les tuyaux. Aux dernières nouvelles, ça avance très bien et les résultats sont la.

    • [^] # Re: PyPy-STM

      Posté par  (site web personnel) . Évalué à 3.

      Je ne l'ai pas inclus car Ⅰ c'est pas forcément super stable, Ⅱ c'est une version modifiée d'un interpréteur alternative : Pypy, et Ⅲ Python 3 ne semble pas encore supporté.

      Je n'ai pas connaissance d'une bibliothèque pour gérer STM dans Python, ce qui est dommage, car ça serait bien plus simple à utiliser.

      • [^] # Re: PyPy-STM

        Posté par  . Évalué à 3.

        À cause du GIL, STM n'a pas beaucoup d'intérêt, puisque les problèmes cpu bound ne tournent jamais sur plusieurs threads

        • [^] # Re: PyPy-STM

          Posté par  . Évalué à 2.

          En même temps, si j'ai bien compris STM va permettre d'enlever le GIL dans pypy (il y a même une référence dans la faq ).

          Quelque part, j'ai envie de dire que c'est encore plus intéressant!

          • [^] # Re: PyPy-STM

            Posté par  . Évalué à 2.

            Oui, c'est ça, c'est pour ça que c'est une branche de PyPy et pas juste une librairie.

  • # Verrou et RAII

    Posté par  . Évalué à 1.

    En objet avec le principe RAII on peut créer un verrou qui sera libérer par le destructeur de l'objet.sous QT il y les QMutexLocker.
    * On créer un objet QMutexLocker sur la pile
    * Quand le programme quitte la fonction ( return au milieu ou exception ) l'objet QMutexLocker est détruit et le mutex libéré.

    • [^] # Re: Verrou et RAII

      Posté par  (site web personnel) . Évalué à 3.

      C'est vrai, mais ce n'est qu'une manière différente de gérer les verrous, avec ces avantages et inconvénients (objets conçus pour s'occuper d'une ressource, s'assurer de leurs destructions (problèmes de GC), etc.).

      Cette dépêche est plus là pour présenter 3 grandes idées, plutôt que de s’intéresser aux variations de celles-ci.

      Mais merci pour ce détail :)

      • [^] # Re: Verrou et RAII

        Posté par  . Évalué à 6.

        Rust utilise aussi du RAII pour gérer les verrous. Je suis d'accord avec toi quec'est une variation d'un même concept théorique, mais dans la pratique, ça fait une différence énorme. En rust, il est par exemple impossible de lire ou de modifier la valeur protégèe sans verrouiller auparavant le verrou, et il est en outre impossible d'oublier de le déverouiller (sauf à créer une boucle infinie qui garde une référence sur la valeur protégée, mais dans ce cas, le problème est ailleurs).

        Même si c'est théoriquement une variation, quand il s'agit de coder dans la vraie vie, ça fait une différence énorme.

        • [^] # Re: Verrou et RAII

          Posté par  . Évalué à 4.

          Vu comme ça, Java peut aussi l'avoir avec un try-with-resources et une classe dédiée qui implemente Closable.

          Un truc comme ça:

           try  (AutoLock l = lock(monLock)) {
               // fait des choses protégées par un verrou
           } // unlock() appelé par AutoLock.close()

          Je suis sûr que d'autres langages ont des mécanismes équivalents.

          • [^] # Re: Verrou et RAII

            Posté par  . Évalué à 3.

            Absolument, la construction with est courante dans beaucoup de langages. Mais il y a quand même une différence notable, elle est optionnelle est requiert du code spécifique, ce n'est pas la méthode unique et par défaut qui empêche toute mauvaise utilisation.

            • [^] # Re: Verrou et RAII

              Posté par  . Évalué à 5.

              Tout à fait. Par contre les with et try-with-resource (qu'on trouve en python, java, C#) :

              • permettent de visualiser qu'une instruction a un coût
              • on peut palier au langage avec de l'analyse statique de code (j'ai vu en Java des analyseurs qui crient sur un objet Autoclosable - qui peut s'utiliser dans un try-with-ressources - ne sont pas utilisé dans des try-with-resources)

              C'est pas parfait mais ça aide déjà pas mal.

              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

              • [^] # Re: Verrou et RAII

                Posté par  . Évalué à 2. Dernière modification le 24 juin 2015 à 11:06.

                C'est pas parfait mais ça aide déjà pas mal.

                Je suis d'accord sur ce point, j'utilise aussi ce genre de construction en Ocaml par exemple, ou il est d'usage de faire cela avec des clotures, même sans construction dans le langage lui même:

                   Lock.with_lock l (fun _ -> now_do; something )

                permettent de visualiser qu'une instruction a un coût

                Je ne comprends pas où tu veux en venir avec cette affirmation. Je ne connais pas vraiment de construction qui cache le verrouillage. Si tu opposes ça au RAII décrit plus haut, en rust tu écrirais :

                 let locked_int = RWLock::new(42);
                 let mut int_ref = locked_int.write();
                 if int_ref < 12 {
                    *int_ref = 12;
                  }
                 // Ici le verrou est relaché de manière implicite par le destructeur de `int_ref`

                Je pense qu'en C++ la construction est similaire, le verrouillage (ce qui est vraiment couteux) est explicite, seul le déverouillage est implicite. Ce qui est un peu le cas avec "with" aussi, le deverouillage arrive à la fin du scope lexical du block with.

                • [^] # Re: Verrou et RAII

                  Posté par  . Évalué à 2.

                  En C++

                  on crée un objet mutex qui utilise les fonctions mutex du système

                   class mutex{
                     system_mutex mut;
                   public:
                     mutex(){system_mutex_init(mut);}
                     void lock(){system_mutex_lock(mut);}
                     void unlock(){system_mutex_unlock(mut);}
                  };

                  puis un objet mutexeLocker qui gère :
                  * le verrouillage dans le constructeur
                  * le déverrouillage dans le destructeur

                   class mutexeLocker {
                     mutex &mut;
                   public:
                     mutexeLocker (mutex &_mut):mut(_mut){mut.lock();}
                     ~ mutexeLocker (mutex &_mut):mut(_mut){mut.unlock();}
                  };

                  exemple d'utilisation :

                  mutex mut_de_var;
                  string var="";
                  
                  void f(string &str)
                  {
                    mutexLocker mutLocker(mut_de_var); //verrou
                    var = str;
                    fonction_avec_execption();
                    cout << str
                   //destruction de mutlocker déverrouillage implicite 
                  }
                  
                  void thread_1()
                  {
                    int cpt=0;
                    while(1){
                    try(){
                    f(string("thread1 : ")<<cpt );
                    }
                    catch(){}
                   }
                  }
                  
                  
                  void thread_2()
                  {
                    int cpt=0;
                    while(1){
                    try(){
                    f(string("thread2 : ")<<cpt );
                    }
                    catch(){}
                   }
                  }

                  bon sous Qt et boost tout est fait et certainement mieux fait :)

                  • [^] # Re: Verrou et RAII

                    Posté par  . Évalué à 3.

                    Je vois. En rust, on utilise en proxy. La méthode write() retourne une structure qui contient un pointeur vers le mutex. Le destructeur de cette structure deverouille le mutex. Ensuite, la magie du trait Deref/DerefMut entre en jeux. Cela revient à surcharger l'implémentation de * ou de ->. Ainsi, le compilateur va considérer cette structure comme une référence vers la valeur protégée par le mutex.

                    De plus, grace au lifetime, il est impossible de conserver une reference vers la valeur protégée après avoir dévérouillé le verrou, c'est garranti à la compilation. C'est assez malin. Je pense que tu peux faire ça en C++, sauf peut être la dernière garrantie car rien n'interdit en C++ de cloner des references.

                    • [^] # Re: Verrou et RAII

                      Posté par  . Évalué à 1.

                      Je ne suis pas sur d'avoir compris ce que tu dis, mais pour :

                      Je pense que tu peux faire ça en C++, sauf peut être la dernière garrantie car rien n'interdit en C++ de cloner des references.

                      … je pense qu'on peut faire quelque chose avec les pointeurs intelligents dérivés de unique_ptr.

                      bépo powered

                • [^] # Re: Verrou et RAII

                  Posté par  . Évalué à 5.

                  le verrouillage (ce qui est vraiment couteux) est explicite

                  Ce n'est pas le verrouillage qui est coûteux, mais la section de code qui est protégée. C'est déjà bien que la prise du verrou ne soit pas implicite, mais c'est (AMHA) moins lisible de ne pas représenter la section de code protégée.

                  Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                  • [^] # Re: Verrou et RAII

                    Posté par  . Évalué à 2. Dernière modification le 24 juin 2015 à 14:53.

                    L'indentation est là pour ça. Tu peux aussi mettre des { } autour de la section si tu préfères.

  • # Verrous, sémaphore et mutex

    Posté par  . Évalué à 4.

    Merci pour ce texte.
    Puisque ce texte se veut avant tout didactique et pédagogique, cela mériterait d'être plus précis sur les différences entre verrous, sémaphore et mutex, surtout que le bout de code donné en exemple (tant que …) ressemble plus à une attente active…
    Or l’intérêt du sémaphore, c'est justement d'endormir le tread/processus appelant si le sémaphore est vide. Du coup, une présentation des primitives P et V peut alors être utile.

    Un mutex est lui aussi un sémaphore binaire, mais qui connaît celui qui possède son jeton. Il n'y a donc pas de blocage si un même thread tente de prendre plusieurs fois le même mutex (sans l'avoir libérer auparavant). Avec un sémaphore binaire, vous vous retrouveriez bloqué…

    • [^] # Re: Verrous, sémaphore et mutex

      Posté par  (site web personnel) . Évalué à 5.

      Un mutex est lui aussi un sémaphore binaire, mais qui connaît celui qui possède son jeton.

      Ou pas. C'est un détail d'implémentation - certains mutex sont réentrants mais ça n'est pas obligatoire (pour les les mutex posix c'est une option, sous Win32 c'est de base).

      Python 3 - Apprendre à programmer dans l'écosystème Python → https://www.dunod.com/EAN/9782100809141

    • [^] # Re: Verrous, sémaphore et mutex

      Posté par  (site web personnel) . Évalué à 3.

      Ce n'est que du pseudo-code, montrant simplement l'idée générale du principe.

      ressemble plus à une attente active

      Ça dépend du point de vue, et c'est pas l'objet de cette dépêche.

      Du coup, une présentation des primitives P et V peut alors être utile.

      C'est un détail ici. Cette dépêche n'a pas pour but de rendre expert le lecteur, mais simplement d'avoir une idée de principe claire. Néanmoins, si tu souhaites développer sur les sémaphores, et expliquer comment la valeur d'un sémaphore est incrémentée (V) ou décrémenté (P), je t'en pris, et je t'en remercie par avance.

      • [^] # Re: Verrous, sémaphore et mutex

        Posté par  . Évalué à 2.

        Dans mes études j'ai eu du mal a comprendre et appliquer le principe du modèle producteur/consommateur et puis un jour la révélation :
        * le producteur compte le nombre de place libre dans le tuyau et il bloque quand il n'y a plus de place libre
        * le consommateur compte le nombre de place "occupé" ou le nombre d'élément dans le tuyau et bloque si il n'y a plus rien
        * donc il nous faut
        ** un semaphore qui compte les places libre
        ** un semaphore qui compte les places occupée

        Le producteur
        * décrémente les places libres (sem_wait(place_libre) )
        ** bloque si place libre égale 0
        * produit
        * incrémente place occupée (sem_post(place_occupé) )

        Le consommateur
        * décrémente les places occupées (sem_wait(place_occupé) )
        ** bloque si place occupée égale 0
        * consomme
        * incrémente place libre (sem_post(place_libre) )

        Mais j'ai oublié la signification de V() et de P()

        • [^] # Re: Verrous, sémaphore et mutex

          Posté par  (site web personnel) . Évalué à 7. Dernière modification le 22 juin 2015 à 16:55.

          Mais j'ai oublié la signification de V() et de P()

          « P et V du néerlandais Proberen et Verhogen signifient "tester" et "incrémenter" » (source)

        • [^] # Re: Verrous, sémaphore et mutex

          Posté par  . Évalué à 8.

          Mais j'ai oublié la signification de V() et de P()

          P comme "Puis-je en prendre un jeton ?"
          V comme "Vas y gros ! (j'en rajoute un)"

          (c'est mon mnémotechnique en français, je ne comprend pas le néerlandais…)

          Please do not feed the trolls

        • [^] # Re: Verrous, sémaphore et mutex

          Posté par  (site web personnel) . Évalué à 2.

          • donc il nous faut ** un semaphore qui compte les places libre ** un semaphore qui compte les places occupée

          Un truc m'échappe : places libres = taille du tuyau - places occupées
          Non ?

          Du coup, pourquoi ne pas parler QUE de places occupées ou QUE de places libres dans le reste de ton énoncé ?

          • [^] # Re: Verrous, sémaphore et mutex

            Posté par  . Évalué à 2.

            un sémaphore est bloquant, que si son compteur interne est égal à 0.
            Si la taille du tuyau est infini, on n'a pas besoin de compter les places libres, le producteur peut produire sans contrainte, donc le système à besoin que d'un sémaphore

            J'ai oublié de parler de l'initialisation :
            au début on initialise les sémaphores:
            * le sémaphore, qui compte les places occupées, est initialisé à 0
            ** il n'y a pas eu, encore, de production
            ** les consommateurs seront bloqués en attente d'une production
            * le sémaphore, qui compte les place libre, est initialisé à la taille du tuyau
            ** le producteur peut produire autant d'élément qu'il y a de la place dans le tuyau

            Ce système fonctionne si la production et la consommation ont une vitesse moyenne équivalente, sinon le plus rapide attendra le plus lent.

            • [^] # Re: Verrous, sémaphore et mutex

              Posté par  (site web personnel, Mastodon) . Évalué à 2.

              Ce système fonctionne si la production et la consommation ont une vitesse moyenne équivalente, sinon le plus rapide attendra le plus lent.

              En général, il y aura plusieurs producteurs et plusieurs consommateurs, sans quoi l’intérêt est limité.

    • [^] # Re: Verrous, sémaphore et mutex

      Posté par  . Évalué à 3.

      Puisque ce texte se veut avant tout didactique et pédagogique, cela mériterait d'être plus précis sur les différences entre verrous, sémaphore et mutex

      À mon sens non… Ce texte n'est pas une introduction verrous, mutex, et sémaphore, mais un tour d'horizon des grands paradigmes de programmation concurrente : mémoire partagée, acteurs, et mémoire transactionnelle. L'essentiel est là avec la mémoire partagée, les processus s'entendent pour ne pas se marcher sur les pieds en communicant explicitement.

      surtout que le bout de code donné en exemple (tant que …) ressemble plus à une attente active…

      Certes, l'efficacité des implémentations sont intéressantes. Mais ce texte ce focalise plus sur les problématique de génie log. que de performances. Typiquement, les implémentations des mutexes sont très efficace, mais personne ne veut avoir à programmer avec ça, sauf s'il n'a pas le choix, parce que c'est affreusement difficile.

      Please do not feed the trolls

  • # Retour sur les objets actifs

    Posté par  . Évalué à 1.

    Je continue dans mes remarques sur ce texte.

    Concernant les approches "objets actifs", plus que l'aspect message (qui n'est qu'un moyen de communiquer entre objet), j'insisterai plus sur l'aspect comportement et événement des objets actifs.
    A un objet actif est associé une description comportementale (une machine à état typiquement) qui explique comment l'objet réagit (ou non) à des événements (internes ou externes). Effectivement, l'arrivé de message (envoyé par d'autres objets) génère un événement. Mais ce n'est qu'un sous ensemble des événements possibles arrivant sur l'objet actif.
    Concrètement, un événement déclenche le franchissement d'une transition, franchissement déclenchant des actions de l'objet. L'arrivée ou la sortie d'un état peuvent aussi déclencher des actions.

    Ces approches sont effectivement très intéressantes, mais ont aussi des limites : par exemple, impossible d'avoir des traitements bloquants lors des franchissements de transitions. Cela nécessite un certain soin dans leur conception.

    Des framework ont été développés pour ce genre d'approche quand le langage de programmation utilisé ne supporte pas les concepts nativement. En C/C++, je suis assez fan de http://www.state-machine.com/psicc/.

    • [^] # Re: Retour sur les objets actifs

      Posté par  (site web personnel) . Évalué à 2.

      A un objet actif est associé une description comportementale (une machine à état typiquement)

      Rien n'oblige à ce qu'un objet actif soit une machine à état formellement. L'instance de ta classe se trouve dans un processus à part entière, et réagit aux messages reçu (qui n'appellent que les méthodes de ta classe en fait).
      Certes, ton objet actif se retrouve donc avec un état local, qui n'est pas accessible directement de l'extérieur.

      Par contre, où je te rejoins, c'est que cela demande de concevoir son application différemment, en forçant plus sur la modularité. Néanmoins, l'avantage que je préfère des acteurs, c'est que ça permet de créer un système bien plus robuste, dans le sens où chaque acteur peut planter sans faire planter l'application entière : on relance un acteur identique, ou en a déjà plusieurs acteurs qui font le même boulot, et ils traiteront les messages suivants.

      • [^] # Re: Retour sur les objets actifs

        Posté par  . Évalué à 1.

        Effectivement la description comportementale n'est pas forcément une machine à état.
        Je trouvais pratique d'en parler pour décrire le principe de fonctionnement de l'objet actif (en me basant sur les notions classiques des MAE : événement, transition et etat).

        Moi aussi, je suis un grand fan des objets actifs.

        • [^] # Re: Retour sur les objets actifs

          Posté par  . Évalué à 5. Dernière modification le 22 juin 2015 à 22:05.

          Moi aussi, je suis un grand fan des objets actifs.

          Être fan des acteurs je comprends. Des objets actifs je ne comprends pas.

          Un objet actif c'est masquer derrière un proxy ayant une sémantique orientée objet quelque chose qui ne l'est pas du tout (appel de méthode local synchrone VS dispatch distant asynchrone). C'est très vendeur sur les hello world mais en pratique c'est une leaky abstraction immonde qui empêche de designer correctement son application et de gérer les cas d'erreurs.

          C'est grosso modo le même problème que les systèmes de type RPC, connu depuis très longtemps mais après 20 ans on arrive toujours au même conclusion.

          Un modèle à objet actif, je veux bien au sein d'un unique processus mais ça me semble un mauvais compromis, vu que t'as le pire de deux mondes entre un truc mono-process bien designé qui va vite et une abstraction solide pour exprimer du parallélisme potentiellement distribué.

          [Note: vision biaisée d'un ex-mainteneur d'un framework à objet actifs…]

          • [^] # Re: Retour sur les objets actifs

            Posté par  (site web personnel) . Évalué à 5.

            Un objet actif c'est masquer derrière un proxy ayant une sémantique orientée objet quelque chose qui ne l'est
            pas du tout (appel de méthode local synchrone VS dispatch distant asynchrone).

            Ben euh, non: un objet c'est l'encapsulation d'un état local et des fonctions qui permettent de faire évoluer cet état. Que ça soit mis en oeuvre avec des classes et des méthodes plutôt que des threads et des fonctions ne change pas grand chose à l'affaire.

            Dans un langage comme erlang (ou erlang tout court, je ne connais pas vraiment d'équivalent), l'absence de sémaphores, états globaux et autres c***ies du genre, autorisent une telle optimisation du runtime qu'il n'est pas déconnant de concevoir une application avec un objet = un thread.

            cf. https://www.sics.se/~joe/ericsson/du98024.html ou http://www.lshift.net/blog/2006/09/10/erlang-processes-vs-java-threads/

            C'est très vendeur sur les hello
            world mais en pratique c'est une leaky abstraction immonde qui empêche de designer correctement son application
            et de gérer les cas d'erreurs.

            Justement pas, comme cité par un autre commentaire, un objet actif est la plupart du temps assez indépendant pour pouvoir crasher et redémarrer sans embêter les autres.

            C'est grosso modo le même problème que les systèmes de type RPC, connu depuis très longtemps mais après 20 ans
            on arrive toujours au même conclusion.

            Des précisions ?

            Un modèle à objet actif, je veux bien au sein d'un unique processus mais ça me semble un mauvais compromis, vu
            que t'as le pire de deux mondes entre un truc mono-process bien designé qui va vite et une abstraction solide
            pour exprimer du parallélisme potentiellement distribué.

            [Note: vision biaisée d'un ex-mainteneur d'un framework à objet actifs…]

            [Note: vision biaisée d'un développeur heureux qui a finalement trouvé erlang après des années sus des systèmes distribués qui finissent tous par devenir complètement ingérables et/ou pas scalables du tout]

            "Liberté, Sécurité et Responsabilité sont les trois pointes d'un impossible triangle" Isabelle Autissier

            • [^] # Re: Retour sur les objets actifs

              Posté par  . Évalué à 3.

              Dans un langage comme erlang

              Tu parles d'Erlang. Mais sauf si quelque chose à changé ces dernières années, Erlang expose un modèle à acteur. C'est à dire une API qui expose le message passing et la gestion d'erreur qui doit en découler. Ce design DOIT être au cœur de ton application.

              Si tu vas sur la JVM, tu as Akka qui fait la même chose et qui dit la même chose que moi sur les limitations des objets actifs

              Bref si tu es satisfait des acteurs d'Erlang, on dit la même chose…

              plutôt que des threads et des fonctions ne change pas grand chose à l'affaire.

              Dès que tes communications deviennent asynchrone gloups…

              Des précisions ?

              Lis le deuxième papier, il fait quatre page. "Convenience Over Correctness" le résume bien. Un carré ne sera jamais rond.

              sur des systèmes distribués

              Si tu parles de système distribué, alors ne pas masquer la sémantique d'un envoi de message hors du processus est vital. Les acteurs sont excellent à ça, jamais vu une implémentation d'objet actif réussir à le faire correctement (ou plus élégamment que des acteurs).

              • [^] # Re: Retour sur les objets actifs

                Posté par  (site web personnel) . Évalué à 4.

                Ok, j'aurais mieux fait de lire le papier d'abord ;)

                Effectivement, expliciter le message passing est indispensable (avec la gestion d'erreurs qui va bien avec), surtout que ça peut être fait simplement. Ensuite, on a toujours le moyen de rajouter une petite abstraction (gen_server) pour choisir de faire du synchrone/asynchrone mais la base est là.

                En fait, je suis malheureusement entièrement d'accord avec sa conclusion: "mais pourquoi n'a-t-on pas commencé par faire du message queuing au lieu du RPC depuis le début ?". Et je rajouterais: mais pourquoi erlang est-il tant snobé par la communauté scientifique ?

                "Liberté, Sécurité et Responsabilité sont les trois pointes d'un impossible triangle" Isabelle Autissier

                • [^] # Re: Retour sur les objets actifs

                  Posté par  . Évalué à 2.

                  RPC permet de faire du synchrone, c’est plus proche du modèle « habituel », ça explique que les gens aient voulu pousser ça : le développeur n’a pas à changer ses habitudes. Même si au final, c’est plutôt une mauvaise idée.

                  Sinon, pour Erlang et la communauté scientifique, c’est surtout que la communauté scientifique travaille plutôt sur des modèles que sur du code, et donc, utilise des langages plus restreints (bip pour ne citer que lui, par exemple). Mais le modèle utilisé par erlang est énormément étudié et utilisé par la communauté scientifique.

                  Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

          • [^] # Re: Retour sur les objets actifs

            Posté par  . Évalué à 2. Dernière modification le 23 juin 2015 à 11:56.

            Bonjour,

            Je ne suis pas certain de bien comprendre toutes tes remarques. Or, vu ton expérience d'ex mainteneur d'un framework d'objet actif, j'aimerais être certain de bien comprendre tes remarques.

            Un objet actif c'est masquer derrière un proxy ayant une sémantique orientée objet quelque chose qui ne l'est pas du tout (appel de méthode local synchrone VS dispatch distant asynchrone).

            Pour moi, les com entre objets actifs sont toujours asynchrones (par exemple par envoi de message dans des boites aux lettres). Tu es d'accord avec cela ?
            Si oui, en gardant cette analogie avec les BAL, puis-je considérer que le proxy, c'est celui qui ecrit dans la BAL (ou les BAL) de l'objet actif (ou des objets actifs) les messages, c'est bien cela ? L'objet actif lui est en attente en lecture sur sa BAL. Des qu'un message arrive, il le traite alors et exécute ou non ses propres méthodes internes.

            Bref, pour moi, cela reste fondamentalement asynchrone et très conception orienté objet (En conception OO, les objets communiquent par envoi de message).

            C'est grosso modo le même problème que les systèmes de type RPC, connu depuis très longtemps mais après 20 ans on arrive toujours au même conclusion.

            Du coup, je ne comprend pas cette comparaison avec le RPC (qui est pour moi une com synchrone, on reste dans l'appel de méthode avec attente, qu'il soit distant ou non).
            Surtout que dans l'article que tu donnes, ce qu'ils me semblent critiquer dans le RPC, c'est justement cette volonté de vouloir conserver l'illusion du synchronisme alors que dans un monde distribué, c'est plutôt l'asynchrone qui est de mise.

            Effectivement, ce postulat d'asynchronisme des com oblige le concepteur à penser à pas mal de chose (par exemple, tout besoin de com synchrone doit être transformée en 2 com asynchrone). Ce travail supplémentaire, c'est cela que tu critiques ?
            Pour ma part, je trouve cela justement bien car on ne laisse rien d'implicite.
            Et du coup, je comprend pas ta remarque sur les erreurs (j'imagine de com). Car pour moi, elles doivent être gérées par le concepteur (avec des com asynchrone, on n'a pas de garantie sur la réception du message, plus précisément par sa prise en compte par l'appelé). Il n'a pas le choix, et je ne comprend pas ta remarque sur ce pb de la gestion des erreurs.

            Un modèle à objet actif, je veux bien au sein d'un unique processus mais ça me semble un mauvais compromis, vu que t'as le pire de deux mondes entre un truc mono-process bien designé qui va vite et une abstraction solide pour exprimer du parallélisme potentiellement distribué.

            La non plus, je ne comprend pas trop le problème. Pour moi, un objet actif ne présente justement que des avantages pour passer à du multi-tâche car il permet d'isoler les parties ayant un vrai besoin de parallélisme (par rapport à la logique métier de l'applicatif). Ensuite, le mapping un objet actif = un thread/process/tache n'est pas toujours aussi simple certes, mais cela marche souvent.

            [Note: vision biaisée d'un ex-mainteneur d'un framework à objet actifs…]

            Ca m'intéresse de savoir lequel car utilisant celui de Quantum Leap (voir lien donné dans mon post précédant), je le trouve super et ne vois pas trop de problème.

            • [^] # Re: Retour sur les objets actifs

              Posté par  . Évalué à 3.

              Bref, pour moi, cela reste fondamentalement asynchrone et très conception orienté objet

              Je suis aussi surpris que toi. Ce genre de chose, on le voit aussi par exemple dans OSGi où les objets ont leur propre cycle de vie.

              Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

  • # Concurrence / Parallélisme

    Posté par  (Mastodon) . Évalué à 10.

    Je ne suis pas d'accord avec tes définitions de concurrence et parallélisme.

    La concurrence est d'abord le fait de partager un bout de la mémoire avec plusieurs processus (ou fils d'exécutions).
    Le parallélisme consiste en l'exécution de plusieurs processus en même temps, sur des données disjointes.

    Pour moi, la concurrence, c'est la description de tâches et de leurs dépendances éventuelles. Le parallélisme, c'est une exécution de tâches concurrentes par un ensemble de threads. Voir aussi Concurrency is not Parallelism, une présentation de Rob Pike (un des inventeurs du langage Go) qui dit sensiblement la même chose.

    Tes définitions me semblent fausses. Typiquement, quand j'exécute une commande avec un pipe, j'ai deux processus concurrents qui ne partagent pas de mémoire (ils partagent des données via un mécanisme de synchronisation, le pipe). Et inversement, quand je fais du parallélisme avec des threads, je peux avoir des données partagées (c'est bien à ça que servent les mécanismes types mutex, à protéger les données partagées).

    Auparavant, la distinction entre concurrence et parallélisme n'était pas aussi évidente parce que les deux étaient mélangés. Dans OpenMP ou MPI, on décrit la concurrence entre les tâches mais on a aussi la manière dont l'exécution va se passer. Avec les threads, c'est pareil, les deux sont intimement liés. Ces dernières années, on s'est aperçu que décrire la concurrence entre les tâches sans s'occuper de savoir comment elles vont être exécutées étaient une meilleure stratégie parce qu'alors, on peut utiliser des modèles de parallélisme adaptés à chaque situation. Par exemple, dans Go, on décrit la concurrence avec des goroutines et des channels mais derrière, ça ne dit rien sur la manière dont l'ensemble sera exécuté (et on peut l'exécuter sur un seul thread si on veut).

    À mon avis, cette manière de faire a de l'avenir, parce qu'on peut alors séparer le travail en deux : trouver les bons modèles de concurrence, c'est-à-dire les bonnes abstractions qui permettent de raisonner facilement sur le programme (les goroutines/channels sont un exemple mais il y en a beaucoup d'autres); trouver les bons modèles de parallélisme, même si, à mon avis, le travail est déjà plus avancé de ce côté.

    • [^] # Re: Concurrence / Parallélisme

      Posté par  (site web personnel) . Évalué à 3.

      Techniquement, mes définitions ne sont pas mauvaises.

      Voici ce que dit le CNRTL :

      # concurrence
      B1. Fait de se trouver en opposition, le plus souvent d'intérêt dans la poursuite d'un même but, chacun visant à supplanter son rival.
      
      # parallèle
      B2. Traitement/transferts parallèle(s). ,,(...) qui sont menés de front, chacun utilisant seul au même instant un organe différent`` (scom Informat. 1977).
      

      Le problème vient de l'anglais, dont les mots concurrence et parallel ont des sens identiques à l'origine : plusieurs choses se passant au même instant.

      C'est ce qui a amené à la distinction, plus tard, dans le milieu informatique, mais tout le monde n'est pas d'accord (ou pas encore).

      Le problème de ma définition de parallélisme, c'est le données disjointes finalement. Voici une mise à jour :

      La concurrence est d'abord le fait de partager un bout de la mémoire avec plusieurs processus (ou fils d'exécutions).
      Le parallélisme consiste en l'exécution de plusieurs processus en même temps, éventuellement en concurrence.
      

      Ce n'est pas la définition que tu proposes (assez opposée), mais elle est bien plus naturelle d'un point de vu vocabulaire je trouve.

      • [^] # Re: Concurrence / Parallélisme

        Posté par  . Évalué à 8. Dernière modification le 23 juin 2015 à 08:50.

        Je suis d'accord avec le premier commentaire, la concurrence n'est pas réduite à la concurrence sur la mémoire. La définition du CNRTL que tu reproduis ici n'est d'ailleurs pas spécifique.

        La concurrence, c'est le fait de partager des ressources, pas uniquement de la mémoire. Ça peut être du CPU (c'est d'ailleurs à mon avis ce que partagent le plus souvent les threads coopératifs), mais aussi une connection réseau (le pipelining HTTP est typiquement ce que j'ai envie d'appeler concurence).

      • [^] # Re: Concurrence / Parallélisme

        Posté par  (site web personnel) . Évalué à 7.

        Le problème vient de l'anglais, dont les mots concurrence et parallel ont des sens identiques à l'origine : plusieurs choses se passant au même instant.

        C'est le problèmes des faux-amis. Concurrent (en anglais) c'est concomitant, simultané, pas concurrent !

  • # pour aller plus loin

    Posté par  (site web personnel) . Évalué à 5.

    Une excellente référence/introduction sur le sujet : Is Parallel Programming Hard, And, If So, What Can You Do About It?
    https://www.kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html

    C'est plutôt orienté programmation système bas niveau mais ça me semble une saine lecture pour toute personne qui s'intéresse au sujet même dans un cadre plus applicatif.

    pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

    • [^] # Commentaire supprimé

      Posté par  . Évalué à -10. Dernière modification le 07 juillet 2015 à 08:39.

      Ce commentaire a été supprimé par l’équipe de modération.

  • # Mon expérience

    Posté par  . Évalué à 5.

    Exposé du problème

    Pour moi dans la diversité des problèmes, l'énorme majorité se placent dans un cas trivial. Il est très rare (de mon expérience toujours) d'avoir des cas d'état partagé ou de processus (léger ou pas) qui doivent réellement coopérer. En architecturant le problème autour de patterns adaptés, on peut s'affranchir de l'utilisation des mutex/sémaphore/moniteur. Le pipelining (aujourd'hui si on veut être in, on parle de reactive stream, c'est différent mais la logique de conception reste la même) est un exemple qui parle beaucoup ici de manière de faire du parallélisme très simplement sans jamais avoir à gérer d'exclusion mutuelle1. Les « CEP » pour complexe event processing sont aussi une autre manière de faire assez puissante.

    Par contre, j'ai encore du mal à voir les cas d'utilisations des acteurs (qui restent si je ne m'abuse centré sur de l'envoi de message donc sans état partagé).


    1. Ce que j'apprécie avec ce genre de démarche c'est qu'elle limitent le nombre de processus créer ce qui permet d'éviter de perdre du temps dans des ordonnanceurs. 

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Mon expérience

      Posté par  . Évalué à 2.

      Je suis plutôt d'accord avec ce point de vue, mais le problème c'est que parfois tu ne peux pas repenser l'architecture du code pour ne pas avoir d'état partagé à cause de contraintes externes.

      C'est peut être un exemple de niche, mais c'est le premier qui me vient à l'esprit : l'api POSIX. Si tu veux implémenter un noyau respectant l'API POSIX, tu es quasiment obligé d'avoir des états partagés à beaucoup d'endroits (même si tu peux t'en passer la plus possible), parce que l'api a justement étée concue dans cette optique : le système de fichier est un espace global partagé.

      Evidemment, tu peux inventer de nouveaux types de noyau, par exemple basés sur les capabilities, auquel tu peux appliqué le model acteur et organisé la cooperation des différents composants du système à l'aide de messages et d'états locaux en utilisant les capabilities comme noms vers un état conservé chez le processus distant, voire créer quelque chose de complétement stateless globalement ayant juste des états locaux par sous-système et par cpu par sous-système en utilisant des techniques de hashing pour répartir la charge.

      Mais cela requiert souvent de modifier l'API que tu exportes. Encore une fois, les noyaux ne sont qu'un exemple, c'est aussi le cas pour des bibliothèques ancienne que tu voudrais mettre à jour pour en améliorer les performances sur les cpu modernes.

      Il y a un autre point important à prendre en compte à mon avis : même si je suis convaincu que ne pas partager d'état global est gagnant sur le long terme parce que cela simplifie beaucoup la manière de raisonner sur des bases de code assez larges, cela peu sembler beaucoup plus contraignant à court terme. En effet, je trouve qu'il y a un effort de reflexion supplémentaire pour créer une architecture basée sur des modules entièrement découplés, comparé à l'ajout de verrous un peu partout.

      En pratique, les paradigmes de programmation principaux, que ça soit la programmation orientée objet ou la programmation fonctionnelle sont entièrement basés sur ce principe là et devrait conduire à des design qui se parallélise sans trop d'effort, mais force est de constater que ça reste de la responsabilité du programmeur d'archicturer le code proprement. No silver bullet, le langage ne te force jamais à faire les choses comme il faut, il fournit juste des abstractions pour le permettre.

      • [^] # Re: Mon expérience

        Posté par  . Évalué à 3.

        Je suis plutôt d'accord avec ce point de vue, mais le problème c'est que parfois tu ne peux pas repenser l'architecture du code pour ne pas avoir d'état partagé à cause de contraintes externes.

        Mon avis là dessus c'est qu'il faut maintenir son code en ayant en tête de réduire l'état global du système. Dis autrement tu crée un sous-système qui ne partage pas son état avec le reste et au fur et à mesure tu fais diminuer les portions de code globales aux profit de portions plus localisée. L'objectif ultime c'est d'avoir des état partagées uniquement dans tes interfaces extérieurs du système et que celles-ci soient le plus localisées possibles.

        Mais ça ne résous pas tout loin de là. Il faut avoir un vrai gros chantier si tu veux te mettre à utiliser un bus d’événement par exemple.

        Mais tout ça ça commence par écrire des fonctions pures quand c'est possible par exemple (= ça ne demande pas forcément beaucoup de boulot).

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Commentaire supprimé

          Posté par  . Évalué à -10.

          Ce commentaire a été supprimé par l’équipe de modération.

          • [^] # Re: Mon expérience

            Posté par  . Évalué à 3. Dernière modification le 29 juin 2015 à 00:23.

            Non… Ou oui. Peut-être…

            En fait on s'en fout. Ta logique métier (par exemple), n'a pas à modifier d'état. Si tu veux, tu l'écris dans une bibliothèque mais ça ne change rien. Tu peux faire l'inverse et avoir tes accès extérieurs dans un framework ou une bibliothèque, c'est plus ou moins ce que font les plupart des frameworks web.

            L'idée général c'est de ne pas écrire au même endroit le code qui récupère les données que celui qui les traite et de découpler le code qui donne un résultat de ce qu'on fait de ce résultat.

            Organiser son code de cette manière donne pleins de propriétés très sympa. Le fais de le mettre dans une bibliothèque n'a rien d'important.

            C'est l'approche stream processing, entre autre poussé par Rx (mais pas que) et magie ça monte très très bien en charge.

            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: Mon expérience

      Posté par  (site web personnel) . Évalué à 2.

      Il est très rare (de mon expérience toujours) d'avoir des cas d'état partagé ou de processus (léger ou pas) qui doivent réellement coopérer.

      C'est également mon expérience. Je programme beaucoup en OCaml qui a une bibliothèque très puissante Lwt pour faire de la programmation concurrente. Comme OCaml est un langage fonctionnel, on est en tant que programmeur assez disposé à utiliser des valeurs contantes et des fonctions récursives pour décrire les traitements. Le fait que les valeurs soient constantes réduit énormément les besoin en mutexes. La bibliothèque Lwt propose des threads collaboratifs – plutôt que le traditionnel modèle préemptif – l'ordonnanceur ne peut changer de thread que lorsque le thread en cours d'exécution atteint un point de collaboration. Cette stratégie réduit d'autant les besoins en mutexes. Par une astuce élégante, le type des fonctions indique clairement si elles contiennent un point de collaboration ou pas, ce qui rend la bibliothèque très facile et agréable à utiliser.

      Le revers de la médaille est que OCaml ne supporte en principe pas le parallélisme, mais on peut cependant faire fonctionner Lwt comme Node.JS où un thread (de type OS) exécute le programme OCaml ou Javascript tandis qu'un second s'occupe des appels systèmes.

      Ce que j'apprécie avec ce genre de démarche c'est qu'elle limitent le nombre de processus créer ce qui permet d'éviter de perdre du temps dans des ordonnanceurs.

      Ça évite aussi de se prendre les pieds dans un drapeau Suisse! :)

      • [^] # Re: Mon expérience

        Posté par  . Évalué à 3.

        Ce que tu présente avec Lwt me fait penser à POE (Perl Object Environment) qui est un moteur d’événements. L'idée c'est que l'interpréteur perl n'est pas forcément compilé avec les options pour gérer des threads systèmes. POE permet d'enregistrer des callback qui vont être déclenché par une boucle d’événement (PEO propose la sienne, mais il peut en prendre d'autre comme celle de GTK par exemple). Comme pour Lwt c'est de la collaboration.

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: Mon expérience

          Posté par  . Évalué à 3.

          J'ai du mal à comprendre le modèle de programmation de POE, vu que je connais pas perl, mais la force de Lwt c'est que ce n'est pas vraiment des callback. Ça en est sous le capot évidemment, mais l'idée c'est de programmer de la même manière que si c'était un thread avec une execution linéaire. C'est l'idées des futures/promises qu'on retrouve dans le nouveau standard javascript.

          • [^] # Re: Mon expérience

            Posté par  (site web personnel) . Évalué à 3.

            Oui Lwt utilise des monades, ce qui permet de programmer en composant des threads un peu comme on compose des fonctions. Même si on programme un peu à la va-comme-je-te-pousse, cela reste à peu près lisible et maintenable, tandis que les paradigmes basés sur des callbacks comme Node.js requièrent uneplus grande discilpline de programmation.

          • [^] # Re: Mon expérience

            Posté par  . Évalué à 3.

            Oui c'est le coté collaboratif qui m'a fait dire ça, mais PEO c'est plus du réactif (on réagis à des événements).

            Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.