Un petit exemple “hello world” Un véritable exemple : mise en place
Transcription
Un petit exemple “hello world” Un véritable exemple : mise en place
L3 Informatique (& Miage TP optionnel) Programmation système et Réseau Université Nice – Sophia Antipolis F. BAUDE Sujet réalisé par O. Dalle et F. Baude Année universitaire 2007/2008 Séance de TP N°11 11.pthread – Utilisation des threads POSIX Il s’agit pour ce TP de prendre contact avec une librairie de threads conforme la norme POSIX pthread. Dans chaque exercice, vous pourrez tester les différences liées aux systèmes d’exploitation sous-jacents (comme par exemple la taille du buffer d’E/S) en compilant puis exécutant chaque programme sur les différentes architectures disponibles : • Sur un PC Linux quelconque pour le système Linux • Sur la machine thalie pour le système Solaris • Sur la machine uranie pour le système OSF/1 Le Makefile fourni permet de compiler un même programme sur différents systèmes en s’adaptant aux spécificités de celui-ci (fichier d’inclusion dalle-thread.h) et surtout en produisant chaque fois un fichier binaire différent. Etudiez bien ce Makefile, vous y découvrirez quelques astuces souvent fort utiles. Créez votre répertoire de TP ~/SR/11phtread et placez-vous dessus. Récupérez dans votre répertoire de TP le fichier Fourniture11.tgz sur le site de cet enseignement et déballez-le. Dans chacun des 2 sous-répertoires, vous remarquerez que vous n’avez pas de Makefile.gen, mais directement un Makefile prêt à l’emploi.. Un petit exemple “hello world” Regardez les fichiers hello.c (version non multi-thread qui affiche “hello world”) puis hellothread.c (version multithread du même programme) situés dans le répertoire Ex1. Regardez attentivement les fonctions de création de threads. Constatez également qu’il faut (1) que la thread hello ait fait son travail avant que la thread world puisse faire le sien; (2) que les 2 threads aient fait leur travail, avant que la thread main écrive le retour chariot. Dans hello-thread2.c, nous avons forcé la thread qui exécute la fonction hello à rendre la main avant d’avoir fait son travail (appel à notre macro YIELD() qui si elle le peut, appelle la fonction sched_yield(2)). Mais, grâce au join dans la thread world, la thread world n’affichera pas son message, tant que la thread hello n’aura pas fait de même et terminé. Pour vous en convaincre, testez les deux exécutables produits à partir de ce fichier source, ces 2 exécutables se nommant : hello-thread2 et hello-thread2-nojoin (dans lequel l’instruction pthread_join est désactivée), et regardez l’effet produit. Un véritable exemple : mise en place d’un filtre Placez-vous maintenant dans le répertoire Ex2 Le programme filter doit transformer en majuscules les caractères lus sur l’entrée standard et afficher le résultat sur la sortie standard. Ce travail est réalisé par plusieurs threads, suivant le modèle producteur/consommateur. Le producteur (thread reader) lit l’entrée standard par blocs, ou buffers, de taille BUFSIZ. Chaque bloc est stocké par la thread reader dans un des NBUF buffers (la zone tampon entre le producteur et le consommateur est donc de taille NBUF). Le consommateur (thread filter writer) prend un buffer rempli par le producteur, en modifie le contenu, et enfin écrit le résultat sur la sortie standard. Compiler puis lancer filter sur un petit fichier (par exemple, sur filter.c). Notez que dans les fonctions alloc_buffer et enable_buffer se cachent toutes les synchronisations entre les 2 threads. En effet, pour qu’un buffer puisse être traité puis vidé, il faut qu’il soit rempli; pour qu’un buffer puisse être rempli, il faut qu’il soit vide ! L3 Informatique (& Miage TP optionnel) Programmation système et Réseau Université Nice – Sophia Antipolis F. BAUDE Sujet réalisé par O. Dalle et F. Baude Année universitaire 2007/2008 Première étape : rajouter une troisième thread Quand vous aurez bien compris les contraintes de synchronisation entre les 2 threads, rajoutez une thread filter qui décharge la thread “writer” de la partie filtrage des données. Pour que ces 3 threads se coordonnent correctement, rajoutez un état ST MODIFIED et transformez les conditions d’attente des threads en conséquence. Expliquez au passage le commentaire de la fonction enable_thread /* dans version originale : cond_signal */ (on vous rappelle que la version originale ne mettait en œuvre que 2 threads). Seconde étape : rajouter une troisième thread A présent, rajouter un fprintf(stderr,...) suivi de fflush(stderr) dans chacune des 3 threads entre les fonctions alloc et enable, de la forme suivante : Je suis la thread ... et je travaille pour l’itération x. Changez aussi le nombre de buffer pour 50 au lieu de 4 (variable NBUF). Puis testez cette modification sur différents systèmes (Linux, OSF, Solaris). Attention au fait suivant : l’ordre d’exécution peut être différent entre la première exécution du programme sur un fichier donné et les exécutions suivantes sur ce même fichier car cela dépend des accès au disque, via le cache. Justement, rappelons quel est le rôle du cache disque • en écriture vers le disque : terminer rapidement l’´ecriture en allant déposer les données en mémoire plutôt que sur le disque • en lecture depuis le disque : de profiter de données qui auraient déjà été chargées depuis le disque et qui se trouvent dans le cache. Premier essai Utilisez filter une première fois sur un gros fichier non encore dans le cache, et dont la lecture peut s’avérer longue. Recherchez un gros fichier accessible en lecture, par exemple l’aide d’une commande find telle que la suivante : find / -type f -size +2000k -perm +111 -print 2> /dev/null Lancez plusieurs fois l’exécution du programme sur ce gros fichier, en veillant à rediriger la sortie d’erreur dans un autre fichier (redirection 2>) pour pouvoir étudier attentivement ce qui se passe au début de l’exécution. Vous pourrez peut-être constater que la thread reader ne remplit pas forcément toutes les cases du tampon avant que les autres aient la main. Sur un très gros fichier comme celui-ci par exemple, /u/profs/buffa/Desktop/setup.app/Contents/Resources/Java/Installer.zip on observe également des comportements non uniformes quant à l’ordonnancement des threads filter et writer. Quel est l’ordonnancement entre les threads ? Maintenant, essayons de qualifier l’ordonnancement de threads mis en œuvre par la librairie de threads. Voici une liste de questions à vous poser : • Est-ce que la thread filter (qui ne fait aucune E/S) a perdu la main au milieu d’un travail ? • Comment se fait-il que parfois une thread arrive à enchainer jusqu’`a NBUF itérations avant de perdre la main ? Et d’ailleurs, pourquoi perd-elle la main ? Avec vos réponses, vous devriez être convaincu que l’ordonnancement entre les threads est tel qu’une thread garde la main tant qu’elle peut s’exécuter. On n’est donc pas en présence d’un ordonnancement à temps partagé. Pour savoir si l’ordonnancement est éventuellement préemptif, il nous faudrait pouvoir allouer des priorités différentes aux 3 threads … cela sort du cadre de ce TP.