Le programme ci-dessous met en relief un exemple d'échange de données synchronisé entre deux threads à l'aide d'une zone de transit synchronisée par voie de mutex.
La liste des en-têtes auxquels nous aurons recours ici est indicative du fait que nous utiliserons les outils de C++ 11. En effet, tous nos en-têtes sont standards, et nous serons en mesure d'accomplir notre tâche en totalité sans avoir recours à des outils propres à une plateforme ou l'autre. D'ailleurs, cette tâche sera, de manière concurrente :
Cette synchronisation a un coût, mais ce coût est probablement moins lourd que vous ne l'auriez cru au préalable. Qu'il soit acceptable ou non variera selon les problèmes et les plateformes. |
|
La zone de transit que nous utiliserons sera générique sur la base du type des valeurs qui y transiteront. Dans notre cas, puisque nous consommerons du texte, le type sera char. |
|
Il peut sembler tentant de faire transiter des string par la zone de transit, du fait que le texte original sera consommé une ligne à la fois, mais mieux vaut résister : extraire une ligne à la fois serait une contrainte abusive pour le consommateur, et utiliser une string comme substrat pour les données de la zone de transit imposerait un coût déraisonnable pour cette structure de données.
Prenez soin de distinguer ce qui importe pour chaque thread, et de réfléchir aux conséquences de vos choix architecturaux.
Le thread qui remplira la zone de transit sera représenté par la fonction lecteur(), alors que le thread qui videra cette zone sera représenté par la fonction scripteur(). Remarquez les paramètres qualifiés volatile, en particulier le booléen fini qui, de plus, est qualifié atomic. Nous y reviendrons. |
|
La fonction append_newline() est dispendieuse avec C++ 03 mais l'est beaucoup moins avec C++ 11, dû à la sémantique de mouvement, qui s'applique ici à la variable anonyme résultant de la concaténation de s avec "\n". |
|
La définition d'une zone de transit reposera sur deux états, soit :
L'implémentation proposée ici repose sur les outils de C++ 11, et expose une interface volatile vouée aux situations multiprogrammées de même qu'une interface non-volatile axée sur la vitesse. Comme c'est la coutume, notre interface volatile procédera en deux temps, soit assurer la synchronisation des opérations par un mutex et déléguer le travail à proprement dit vers un service de l'interface non-volatile. La chose à ne pas faire est d'offrir un service permettant d'ajouter un seul T à la fois : le coût de la synchronisation serait alors trop gros en proportion du coût du traitement qu'il protège., Il est toujours possible d'ajouter un objet à la fois (p. ex. : zt.ajouter(&obj, &obj+1);) mais il ne faut pas encourager des comportements inefficaces en les rendant faciles d'accès. Observez la méthode verrouiller(). Elle est volatile, encapsule les transtypages requis pour capturer le mutex, et retourne un unique_lock possédant le mutex en question à qui souhaite l'utiliser à travers un mouvement (une copie étant bien sûr illégale ici). Ceci permet d'alléger le code client (interne à la classe), et ce en toute sécurité puisque le destructeur du unique_lock en question libérera le mutex quoiqu'il advienne. Portez aussi attention à la méthode extraire(), qui s'exécute en temps constant (complexité ) et pourrait être qualifiée noexcept dans la mesure où le constructeur par défaut d'un vector<T> et son constructeur de mouvement sont tous deux de complexité constante et noexcept. |
|
Reste maintenant à examiner le code de test, et à voir comment les threads qui se partageront la zone de transit sont construits. Nous examinerons d'abord le code de test en tant que tel, qui se déclinera en trois temps :
La fonction de test minuter(f,ntests) accepte une opération nullaire f de type F et l'appelle ntests fois, cumulant la somme des temps écoulés et retournant cette somme. C'est une approche simpliste et éminemment perfectible, mais elle suffira pour nos fins. Si vous souhaitez raffiner le tout, envisagez entre autres :
|
|
La fonction nb_lignes() consomme tous les bytes d'un fichier nommé fich à l'aide d'un istreambuf_iterator et compte le nombre d'occurrences de bytes représentant un saut de ligne. Il est présumé ici que fich soit un fichier texte, bien que ce fichier soit ouvert en mode binaire pour que tous les bytes y soient traités équitablement. |
|
Le programme principal lance les tests en procédant comme suit :
À noter :
|
|
Les threads de ce programme sont très simples.
Le thread lecteur(nom,dest,fini) ouvre le fichier nommé nom, en consomme chaque ligne et l'ajoute (suffixée d'un saut de ligne, du fait que getline() consomme du texte jusqu'à un saut de ligne sans conserver ce dernier) à la zone de transit dest. Une fois le fichier complètement consommé, le thread modifie l'état de fini pour signaler au thread scripteur() qu'il ne l'alimentera plus en données désormais. |
|
Enfin, le thread scripteur(src,nom,fini) consomme le texte de la zone de transit src sous forme d'une string (on aurait aussi pu utiliser des vector<char> ici), consommant chaque fois la totalité des données qui s'y trouvent, et ajoute ce texte au fichier nommé nom. Une fois le signal fini constaté, ce thread consomme une dernière fois les données de src au cas où un dernier remplissage lui aurait échappé, ce qui est tout à fait possible (pour ne poas dire probable) ici. |
|
Un résultat possible de l'exécution de ce programme de test serait :
Temps ecoule: 900 ms. pour 10 tests sur un fichier de 100001 lignes
moyenne: 90 ms. par test