Ce qui suit se veut un petit exemple de solution à un problème proposé informellement en classe, soit celui d'écrire un programme (inefficace, mais c'est pour se faire la main) qui consommera des données d'un fichier source à l'aide d'un fil d'exécution, et rendra progressivement ces données disponibles à d'autres à travers un canal sécurisé (une zone de transit) pour qu'un autre fil d'exécution les consomme pour les écrire dans un autre fichier.
J'ai proposé que vous le fassiez avec un fichier binaire quelconque, et le code ci-dessous fonctionnerait (outre l'affichage en toute fin) tel quel avec un .jpeg ou un .mp3, mais j'ai écrit l'exemple de manière à vous le rendre disponible rapidement alors ma version lit... le code source du programme (fichier Principal.cpp) pour l'écrire dans un autre fichier (sortie.txt), pour enfin en afficher le contenu à la console. Ceci permet de s'assurer qu'il ne manquera aucun caractère en fin de parcours (un réel piège ici).
Remarquez tout d'abord que le code proposé ici est totalement portable à toute plateforme, dans la mesure où un compilateur C++ 11 y est disponible. |
|
J'ai implémenté une zone de transit générique relativement simple. Remarquez l'implémentation de la méthode extraire(), qui est plus efficace que celle (plus naïve) proposée en classe. Comprenez-vous la manoeuvre? J'en profite pour vous rappeler l'importance de conserver un mutex juste assez longtemps, donc pas moins... mais pas plus, car moins votre code sera ralenti par la synchronisation et meilleur en sera le débit de traitement. |
|
Le programme principal partagera deux données entre les fils d'exécution de lecture et d'écriture, soit la zone de transit et un signal de fin. Dans les deux cas, on parle de partage d'une donnée par au moins deux fils d'exécution, dont au moins un en écriture, donc la synchronisation est nécessaire. Pourquoi un signal de fin? Parce que le fil d'exécution qui écrit sur disque ne peut se baser sur la présence ou l'absence de données dans la zone de transit pour déterminer si son travail est terminé. En effet, si le producteur (le fil d'exécution qui lit du disque et remplit la zone de transit) est moins rapide, au moins de temps à autres, que le consommateur (le thread qui lit de la zone de transit pour écrire sur disque), alors il arrivera que le consommateur prenne de l'avance sur le producteur et qu'aucune donnée ne soit disponible pour lui dans la zone de transit... et ce ne sera qu'une situation normale, sans plus. |
|
Le fil d'exécution lecture, producteur au sens de la zone de transit, consommera des données du disque et les insérera en bloc dans la zone de transit (pour éviter une synchronisation trop fréquente). La taille du tampon local (constante N) est à déterminer de manière empirique; pour les besoins de l'exemple, j'ai mis une valeur prise à peu près au hasard. Le signal de fin, donc l'instruction fini = true;, sera donné par le producteur quand il saura que plus aucune donnée ne sera insérée dans la zone de transit, et pas avant. Notez l'ajout dans la zone de transit après la répétitive de lecture, qui permet d'y insérer les données résiduelles du dernier tampon si celui-ci n'est pas rempli. Sans elle, il manquera des données en fin de parcours. |
|
Le fil d'exécution ecriture, consommateur au sens de la zone de transit, extraira le contenu de cette dernière et l'écrira sur disque tant et aussi longtemps que le signal de fin n'aura pas été donné. Notez la dernière extraction après la répétitive, qui permet de vider la zone de transit de toute données ayant été insérée entre le moment de la plus récente extraction et le constat par le consommateur que le signal de fin a été donné. Sans elle, règle générale, il manquera des données dans le fichier en fin de parcours. |
|
Avant de fermer le programme, nous attendons la complétion de l'exécution des deux fils d'exécution, puis (pour fins de débogage) nous affichons le contenu du fichier de destination à la console. Si vous avez copié un fichier contenant des données autres que du texte, présumant que l'extension du fichier de destination corresponde à son contenu, il suffit (du moins avec Microsoft Windows) de remplacer l'appel à copy() par un appel à system() en lui passant le nom du fichier en question, ce qui démarrera le programme associé à ce type de fichier. |
|
Voilà.