I. Introduction▲
La création de scripts Perl Tk a généralement pour but d'éviter
à son utilisateur d'avoir à travailler sous une console noire qui peut faire peur!
Le but est donc de permettre d'interagir
avec une application Perl via une interface graphique.
On est très souvent confronté à des questions existentielles
!
Supposons que notre application Perl/Tk ait pour rôle d'effectuer
de longs calculs, de lister des répertoires, de lancer d'autres
scripts qu'on ne peut pas modifier et en même temps d'afficher
l'heure. Voici les différentes questions que l'on se pose au cours du développement :
- Pourquoi ma fenêtre reste figée quand je clique sur le bouton ?
- Pourquoi la fenêtre ne répond plus et qu'il y a une grosse tâche blanche ?
- Comment dissocier mon calcul de ma fenêtre ?
- Pourquoi l'heure ne bouge plus ?
- Comment suivre la progression d'une tâche ?
- ...
Cet article va essayer de répondre à ces questions. Nous supposerons que vous avez déjà les bases de Perl et bien évidemment de Perl Tk. Si ce n'est pas le cas, les cours de Perl et de Perl/Tk, sans oublier les FAQs sont à votre disposition dans la rubrique Perl.
II. Problématique▲
Afin de vous expliquer au mieux les problèmes rencontrés et comment les résoudre facilement, nous allons réaliser un script qui aura pour but de :
- Afficher l'heure instantanément
- Afficher 3 boutons
un pour fermer l'application
un pour changer l'affichage de l'heure
le dernier pour lister les répertoires
II-1. Première solution▲
Voici le code qui permet de réaliser ce que l'on souhaite. C'est à dire afficher l'heure instantanément et avoir 3 boutons nous permettant de changer l'affichage de l'heure et de lister un répertoire.
#!/usr/bin/perl
#==========================================================================
# Auteur : djibril
# But : Script Perl/Tk pour afficher l'heure, lister un répertoire
#==========================================================================
use warnings;
use strict;
use Tk;
# Creation du widget principal
my $WidgetPrincipal
=
new MainWindow(
-
title =>
"Tk, Thread ou ProgressBar"
,
-
background =>
"white"
,
);
$WidgetPrincipal-
>
minsize( 800
, 200
);
my $Message
=
"rien"
;
my $Heure
=
"L'heure"
;
my $FormatHeure
=
0
;
# Label qui affichera l'heure
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Heure
,
-
background =>
"white"
,
)->
pack
( qw / -fill both -expand 1/
);
# Frame pour les boutons
my $FrameBouton
=
$WidgetPrincipal-
>
Frame(
-
background =>
"white"
,
)->
pack
();
$FrameBouton-
>
Button(
-
text =>
"Fermer"
,
-
command =>
sub {
exit; }
,
)->
grid( -
row =>
0
, -
column =>
0
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
$FrameBouton-
>
Button(
-
text =>
"Changer format de l'heure"
,
-
command =>
sub {
if ( $FormatHeure
==
0
) {
$FormatHeure
=
1
;
}
else {
$FormatHeure
=
0
;
}
}
,
)->
grid( -
row =>
0
, -
column =>
1
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
$FrameBouton-
>
Button(
-
text =>
"Lister des fichiers"
,
-
command =>
sub {
# Selectionner un répertoire
my $Directory
=
$WidgetPrincipal-
>
chooseDirectory(
-
title =>
"Selection d'un repertoire"
,
-
mustexist =>
1
,
);
if ( $Directory
) {
$Message
=
"Listings des fichiers en cours"
;
ListerFichiers($Directory
);
$Message
=
"Listings des fichiers fini"
;
}
}
,
)->
grid( -
row =>
0
, -
column =>
2
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
# Label qui affichera l'etat
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Message
,
-
background =>
"#FFE0D0"
,
-
relief =>
"groove"
,
)->
pack
( qw / -side bottom -fill x -expand 0/
);
# Pocedure pour afficher l'heure
$WidgetPrincipal-
>
repeat( 1000
, \&
AfficherHeure );
MainLoop();
sub AfficherHeure {
# heure
my ( $Second
, $Minute
, $Hour
, $Day
, $Month
,
$Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
) =
localtime
(time
);
$Month
=
$Month
+
1
;
$Year
=
$Year
+
1900
;
foreach ( $Second
, $Minute
, $Hour
, $Day
, $Month
,
$Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
) {
s/^(\d)$/0$1/
;
}
if ( $FormatHeure
==
0
) {
$Heure
=
"(
$Day-$Month-$Year
)
$Hour
"
."::"
.$Minute
."::"
.$Second
;
}
else {
$Heure
=
"
$Hour
"
."::"
.$Minute
."::"
."
$Second
(
$Day-$Month-$Year
)"
;
}
}
#======================================================
# Nombre d'arguments : 1
# Argument(s) : un répertoire ($repertoire)
# Retourne : Tableau de fichier (@fichiers)
#======================================================
sub ListerFichiers {
my ( $repertoire
) =
@_
;
my @fichiers
;
# Ouverture d'un répertoire
opendir
(my $FhRep
, $repertoire
)
or
warn
"impossible d'ouvrir le répertoire
$repertoire\n
"
and
return;
# Liste fichiers et répertoire sauf (. et ..)
my @Contenu
=
grep
{
!
/^
\.\.
?$/
}
readdir
($FhRep
);
# Fermeture du répertoire
closedir
($FhRep
);
# On récupère tous les fichiers
foreach my $nom
( @Contenu
) {
# Fichiers
if ( -
f "
$repertoire
/
$nom
"
) {
push
( @fichiers
, "
$repertoire
/
$nom
"
);
print
"
$repertoire
/
$nom\n
"
;
}
# Repertoires
elsif ( -
d "
$repertoire
/
$nom
"
) {
# recursivité
push
( @fichiers
, ListerFichiers("
$repertoire
/
$nom
"
) );
}
}
return @fichiers
;
}
Si vous exécutez ce script, vous aurez ceci :
Vous remarquerez que l'heure s'affiche toutes les secondes.
Si vous cliquez sur le bouton "changer format…", l'affichage
de l'heure sera modifié. Si vous cliquez sur "Lister fichier", vous devrez
sélectionner un répertoire; tous ces fichiers seront listés.
Le script est ScriptBasique (téléchargeable dans la section
"Téléchargement des scripts").
II-2. Widget figé▲
Si vous listez un répertoire qui contient beaucoup de sous-répertoires et fichiers, vous remarquerez que la fenêtre Tk reste figée tout le temps du listing. De plus, l'heure ne bouge plus. Nous verrons comment palier à ce problème.
II-3. Barre de progression▲
Si l'on souhaite connaître la progression du listing des fichiers, comment peut-on le faire ? Faut-il afficher les noms des fichiers dans la fenêtre Tk au fur et à mesure ? Comment faire une belle barre de progression ? Pour cela, nous utiliserons un module Tk qui est prêt à l'emploi et simple d'utilisation (Tk::ProgressBarTk::ProgressBar).
II-4. Dissocier la tâche de mon widget (threads)▲
Si la méthode de listing provient d'un module externe, comment peut-on l'utiliser sans que la fenêtre ne reste figée ? C'est là qu'intervient la notion de threads (module threadsthreads).
III. Empêcher la fenêtre de se figer▲
Avoir une fenêtre Tk figée est un effet normal. Il indique que votre application
est en train de fonctionner. Pendant que votre script tourne, vous ne pouvez normalement rien
faire car vous n'avez plus la main. Il faut attendre que la tâche en cours se termine. Cet
effet est assez désagréable car l'utilisateur du script ou de l'application n'y est pas
habitué et peut avoir l'impression qu'il y a un bug. De plus, dans notre exemple, on affiche l'heure.
On peut donc trouver cela bizarre que les secondes ne soient pas affichées régulièrement.
Pour palier à ce problème, Perl Tk a prévu une méthode (update) qui permet de rafraîchir la page.
L'idée est donc de faire appel à cette méthode régulièrement, ce qui empêchera le widget d'être figé.
Pour revenir à notre exemple, la procédure qui est susceptible de prendre du temps pendant son exécution
est "ListerFichiers". On va donc la modifier et rajouter un update de la fenêtre à chaque fois
qu'un fichier est trouvé.
Voici donc notre nouvelle procédure "ListerFichiers".
sub ListerFichiers {
my ( $repertoire
) =
@_
;
my @fichiers
;
# Ouverture d'un répertoire
opendir
(my $FhRep
, $repertoire
)
or
warn
"impossible d'ouvrir le répertoire
$repertoire\n
"
and
return;
# Liste fichiers et répertoire sauf (. et ..)
my @Contenu
=
grep
{
!
/^
\.\.
?$/
}
readdir
($FhRep
);
# Fermeture du répertoire
closedir
($FhRep
);
# On récupère tous les fichiers
foreach my $nom
( @Contenu
) {
# Fichiers
if ( -
f "
$repertoire
/
$nom
"
) {
push
( @fichiers
, "
$repertoire
/
$nom
"
);
$Message
=
"
$repertoire
/
$nom
"
; # <=== modification
$WidgetPrincipal-
>
update; # <=== modification
}
# Repertoires
elsif ( -
d "
$repertoire
/
$nom
"
) {
# recursivité
push
( @fichiers
, ListerFichiers("
$repertoire
/
$nom
"
) );
}
}
return @fichiers
;
}
Les principales modifications sont les suivantes :
$WidgetPrincipal-
>
update;
La page sera mise à jour régulièrement, à chaque fichier trouvé.
$Message
=
"
$repertoire
/
$nom
"
;
On a créée un label avec texte variable ($Message)
# Label qui affichera l'etat
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Message
,
-
background =>
"#FFE0D0"
,
-
relief =>
"groove"
,
)->
pack
( qw / -side bottom -fill x -expand 0/
);
A chaque fois que la variable $Message est modifiée, l'affichage change dans le widget.
Garder à l'esprit qu'il existe différentes façons de s'y prendre pour afficher notre information.
On aurait pu choisir de créer un widget Texte scrollé et d'y afficher le résultat du listing au fur et à mesure
avec la méthode "insert", c'est votre imagination qui rentre en compte !
Maintenant, notre script tourne bien, la fenêtre n'est plus figée et c'est plus agréable.
Si je souhaite avoir une barre de progression, que faire ?
Le script est ScriptBasique2 (téléchargeable dans la section
"Téléchargement des scripts").
IV. Création d'une barre de progression▲
Créer une barre de progression est très utile pour avoir une visualisation rapide de l'état d'avancement
d'une tâche, d'un calcul ou autre. Elle est facile à mettre en place, surtout si nous avons
la main sur le calcul à effectuer.
Nous utilisons le module Tk::ProgressBarTk::ProgressBar
et je vous conseille de lire sa documentation. Voici un exemple de code :
my $progress
=
$Widget-
>
ProgressBar(
-
width =>
20
, # Largeur de la barre de progression
-
length
=>
300
, # Longueur
-
from =>
0
, # La valeur initiale est 0
-
to =>
100
, # La valeur de fin est 100
-
colors =>
[ 0
, "#0090F0"
], # La couleur de remplissage de la barre de progression
-
gap =>
0
, # espace en pixel entre chaque block de couleur. 0 donne l'impression d'une continuité.
-
troughcolor =>
"#A5A5A5"
, # Couleur de fond de la barre de progression
)->
pack
( qw / -side bottom -fill x -expand 0/
); # gestionnaire classique de placement
Il existe d'autres options que vous pouvez consulter dans la documentation CPAN. Il existe une seule méthode qui permet de faire progresser la barre. C'est la méthode "value" qu'on doit associer à "update"
# Positionne la barre à 10
$progress-
>
value(10
);
$progress-
>
update;
Pour revenir à notre exemple, l'important est d'être capable de définir une valeur de
départ et de fin pour notre barre de progression, puis de trouver comment la faire progresser
logiquement.
Valeur -from : il sera judicieux de la mettre tout simplement à 0.
Valeur -to : ?
Nous souhaitons lister un répertoire, notre logique sera donc la suivante : calcul de la taille
du répertoire à lister et attribution de cette valeur à "-to". Par la suite, au fur et à mesure
du parcours de notre répertoire, on peut calculer la taille du fichier trouvé et incrémenter une
variable afin de faire progresser notre barre de progression.
Pour calculer la taille d'un répertoire, nous utilisons le module
Filesys::DiskUsageFilesys::DiskUsage,
pour un fichier, l'option -s de Perl. Voici notre script
#!/usr/bin/perl
#==========================================================================
# Auteur : djibril
# But : Script Perl/Tk pour afficher l'heure, lister un répertoire
#==========================================================================
use warnings;
use strict;
use Tk;
use Tk::ProgressBar;
use Filesys::DiskUsage qw/du/
;
# Creation du widget principal
my $WidgetPrincipal
=
new MainWindow(
-
title =>
"Tk, Thread ou ProgressBar"
,
-
background =>
"white"
,
);
$WidgetPrincipal-
>
minsize( 800
, 200
);
my $Message
=
"rien"
;
my $Heure
=
"L'heure"
;
my $FormatHeure
=
0
;
# Label qui affichera l'heure
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Heure
,
-
background =>
"white"
,
)->
pack
( qw / -fill both -expand 1/
);
# Frame pour les boutons
my $FrameBouton
=
$WidgetPrincipal-
>
Frame(
-
background =>
"white"
,
)->
pack
();
$FrameBouton-
>
Button(
-
text =>
"Fermer"
,
-
command =>
sub {
exit; }
,
)->
grid( -
row =>
0
, -
column =>
0
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
$FrameBouton-
>
Button(
-
text =>
"Changer format de l'heure"
,
-
command =>
sub {
if ( $FormatHeure
==
0
) {
$FormatHeure
=
1
;
}
else {
$FormatHeure
=
0
;
}
}
,
)->
grid( -
row =>
0
, -
column =>
1
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
$FrameBouton-
>
Button(
-
text =>
"Lister des fichiers"
,
-
command =>
\&
ListerFichierTk,
)->
grid( -
row =>
0
, -
column =>
2
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
# Label qui affichera l'etat
my $Label
=
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Message
,
-
background =>
"#FFE0D0"
,
-
relief =>
"groove"
,
)->
pack
( qw / -side bottom -fill x -expand 0/
);
# Pocedure pour afficher l'heure
$WidgetPrincipal-
>
repeat( 1000
, \&
AfficherHeure );
# Variable à modifier nous permettant de faire évoluer la barre
my $TailleCourante
=
0
;
my $progress
=
$WidgetPrincipal-
>
ProgressBar(
-
width =>
20
, # Largeur de la barre de progression
-
length
=>
300
, # Longueur
-
from =>
0
, # La valeur initiale est 0
-
to =>
100
, # La valeur de fin est 100
-
colors =>
[ 0
, "#0090F0"
], # La couleur de remplissage de la barre de progression
-
gap =>
0
, # espace en pixel entre chaque block de couleur. 0 donne l'impression d'une continuité.
-
troughcolor =>
"#A5A5A5"
, # Couleur de fond de la barre de progression
)->
pack
( qw / -side bottom -fill x -expand 0/
); # gestionnaire classique de placement
MainLoop();
#==========================
# Nos procédures
#==========================
sub ListerFichierTk {
# Selectionner un répertoire
my $Directory
=
$WidgetPrincipal-
>
chooseDirectory(
-
title =>
"Selection d'un repertoire"
,
-
mustexist =>
1
,
);
if ( $Directory
) {
# initialisation à 0
$progress-
>
value( 0
);
# On calcul la taille du répertoire et initialise -to
$Message
=
"Calcul de la taille du repertoire
$Directory
en cours"
;
$WidgetPrincipal-
>
update;
my $TailleRepertoire
=
du($Directory
);
$progress-
>
configure( -
to =>
$TailleRepertoire
);
# Listing des fichiers
$Message
=
"Listings des fichiers en cours"
;
ListerFichiers($Directory
);
$Message
=
"Listings des fichiers fini"
;
}
return;
}
sub AfficherHeure {
# heure
my ( $Second
, $Minute
, $Hour
, $Day
, $Month
,
$Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
) =
localtime
(time
);
$Month
=
$Month
+
1
;
$Year
=
$Year
+
1900
;
foreach ( $Second
, $Minute
, $Hour
, $Day
, $Month
,
$Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
) {
s/^(\d)$/0$1/
;
}
if ( $FormatHeure
==
0
) {
$Heure
=
"(
$Day-$Month-$Year
)
$Hour
"
."::"
.$Minute
."::"
.$Second
;
}
else {
$Heure
=
"
$Hour
"
."::"
.$Minute
."::"
."
$Second
(
$Day-$Month-$Year
)"
;
}
}
#======================================================
# Nombre d'arguments : 1
# Argument(s) : un répertoire ($repertoire)
# Retourne : Tableau de fichier (@fichiers)
#======================================================
sub ListerFichiers {
my ( $repertoire
) =
@_
;
my @fichiers
;
# Ouverture d'un répertoire
opendir
(my $FhRep
, $repertoire
)
or
warn
"impossible d'ouvrir le répertoire
$repertoire\n
"
and
return;
# Liste fichiers et répertoire sauf (. et ..)
my @Contenu
=
grep
{
!
/^
\.\.
?$/
}
readdir
($FhRep
);
# Fermeture du répertoire
closedir
($FhRep
);
# On récupère tous les fichiers
foreach my $nom
( @Contenu
) {
# Fichiers
if ( -
f "
$repertoire
/
$nom
"
) {
push
( @fichiers
, "
$repertoire
/
$nom
"
);
$TailleCourante
+=
-
s "
$repertoire
/
$nom
"
;
$progress-
>
value( $TailleCourante
);
$WidgetPrincipal-
>
update;
print
"
$repertoire
/
$nom\n
"
;
}
# Repertoires
elsif ( -
d "
$repertoire
/
$nom
"
) {
# recursivité
push
( @fichiers
, ListerFichiers("
$repertoire
/
$nom
"
) );
}
}
return @fichiers
;
}
Ce script vous permet de lister un répertoire et affiche une barre de progression qui évolue pendant
le listing des fichiers sur la console.
Explication du script :
On fait appel au module Filesys::DiskUsage que
vous devez installer sur votre ordinateur. Ensuite, on crée
notre widget Tk. On a modifié la création du bouton "Lister des fichiers"
$FrameBouton-
>
Button(
-
text =>
"Lister des fichiers"
,
-
command =>
\&
ListerFichierTk,
)->
grid( -
row =>
0
, -
column =>
2
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
Lorsque l'on clique sur le bouton, il fait appel à la procédure "ListerFichiersTk", nous y reviendrons plus tard. Par la suite, juste avant Mainloop, nous créons notre barre de progression.
# Variable à modifier nous permettant de faire évoluer la barre
my $TailleCourante
=
0
;
my $progress
=
$WidgetPrincipal-
>
ProgressBar(
-
width =>
20
, # Largeur de la barre de progression
-
length
=>
300
, # Longueur
-
from =>
0
, # La valeur initiale est 0
-
to =>
100
, # La valeur de fin est 100
-
colors =>
[ 0
, "#0090F0"
], # La couleur de remplissage de la barre de progression
-
gap =>
0
, # espace en pixel entre chaque block de couleur. 0 donne l'impression d'une continuité.
-
troughcolor =>
"#A5A5A5"
, # Couleur de fond de la barre de progression
)->
pack
( qw / -side bottom -fill x -expand 0/
); # gestionnaire classique de placement
la variable -from est mise à 0 ($TailleCourante = 0) et -to à 100.
la variable -to sera modifiée par la suite car à ce niveau du script,
nous ne savons pas quelle valeur lui attribuer. Elle
sera modifiée grâce à la méthode Tk "configure".
Revenons maintenant sur la procédure appelée lors d'un clic sur
le bouton "ListerFichiersTk"
sub ListerFichierTk {
# Selectionner un répertoire
my $Directory
=
$WidgetPrincipal-
>
chooseDirectory(
-
title =>
"Selection d'un repertoire"
,
-
mustexist =>
1
,
);
if ( $Directory
) {
# initialisation à 0
$progress-
>
value( 0
);
# On calcul la taille du répertoire et initialise -to
$Message
=
"Calcul de la taille du repertoire
$Directory
en cours"
;
$WidgetPrincipal-
>
update;
my $TailleRepertoire
=
du($Directory
);
$progress-
>
configure( -
to =>
$TailleRepertoire
);
# Listing des fichiers
$Message
=
"Listings des fichiers en cours"
;
ListerFichiers($Directory
);
$Message
=
"Listings des fichiers fini"
;
}
return;
}
On demande à l'utilisateur de choisir un répertoire et nous mettons la barre à 0.
On rafraîchit ensuite la page afin de mettre à jour le message et on calcule la taille
du répertoire. On récupère celle-ci et on l'applique à -to. Notre barre est
donc opérationnelle, on peut donc lister les fichiers.
Dans la procédure "ListerFichiers", vous pouvez remarquer 3 lignes de code
$TailleCourante
+=
-
s "
$repertoire
/
$nom
"
;
$progress-
>
value( $TailleCourante
);
$WidgetPrincipal-
>
update;
La première permet de calculer la taille du fichier, la deuxième fait avancer la barre de
progression et la dernière rafraîchit la fenêtre.
A ce niveau de l'article, on est maintenant capable de créer une barre de progression
dans un script Perl Tk. L'important est de se poser les bonnes questions pour initialiser
l'option -to et de réfléchir à la façon de la faire progresser. Il est donc possible de
faire beaucoup de choses et de déployer une application en Perl Tk fonctionnelle et
robuste. Une fois de plus, c'est l'imagination du développeur qui prime!!
On peut maintenant faire une dernière remarque par rapport à notre exercice. Si on cherche à lister
un très gros répertoire, par exemple le C:/ pour les windowsiens ou le
répertoire /home pour les linuxiens, on s'aperçoit que le calcul de la taille
du répertoire est très long. Pendant ce temps, la fenêtre est figée
(et oui, on n'est pas gâté )! On entre donc dans le cas typique où
l'on ne peut pas intervenir dans la méthode "du" du module pour y faire des "update".
La seule façon pour ne pas avoir une page figée est de faire des threads,
ouf on y arrive !!
Le script est ScriptProgressBar (téléchargeable dans la section
"Téléchargement des scripts").
V. Perl/Tk et les threads▲
V-1. Qu'est ce qu'un thread▲
Un thread en français signifie
processus légerprocessus léger.
C'est un composant du processus principal (votre script).
Chaque thread se partage la mémoire virtuelle du processus mais
possède sa propre pile d'appel (structure de données).
Celui qui utilise des threads a donc l'impression que ces derniers travaillent en parallèle.
Il est important de ne pas confondre processus légers et multitâche dont le
principe est plutôt d'utiliser des processus différents.
Pour plus d'information sur la notion de processus léger, voir la
section liens utiles.
V-2. Exemple basique d'utilisation▲
Pour la suite de cet article, vous devez installer les modules
threads et threads::shared. Ces modules font normalement déjà
partis du CORE de Perl, mais la version présente n'est pas à jour et
il se peut que certaines méthodes utilisées dans cet article ne
fonctionnent pas. Je vous recommande donc de les installer.
Voici un script Perl (non Tk) qui vous permet de créer plusieurs threads. Il a
pour but de créer 10 threads qui afficheront 2 lignes.
#!/usr/bin/perl
#==========================================================================
# Auteur : djibril
# But : Exemple de threads
#==========================================================================
use warnings;
use strict;
use threads;
my @StocageThreads
;
# Création de 10 threads.
for ( 0
.. 9
) {
$StocageThreads
[$_
] =
threads->
create( \&
fun, $_
);
}
print
"Threads crees, passons a autre chose!
\n
"
;
sleep
2
;
print
"Allons recuperer nos valeurs de retours...
\n
"
;
for ( 0
.. 9
) {
print
"thread num
$_
est termine et nous retourne la valeur : "
. $StocageThreads
[$_
]->
join
() . "
\n
"
;
}
sub fun {
my $number
=
shift
;
print
"Bonjour, je suis le thread num :
$number\n
"
;
print
"Mon id est : "
. threads->
tid() . "
\n
"
;
sleep
2
;
print
"le thread num
$number
meurs
\n
"
;
return threads->
tid();
}
Bonjour, je suis le thread num : 0
Mon id est : 1
Bonjour, je suis le thread num : 1
Mon id est : 2
Bonjour, je suis le thread num : 2
Mon id est : 3
Bonjour, je suis le thread num : 3
Mon id est : 4
Bonjour, je suis le thread num : 4
Mon id est : 5
Bonjour, je suis le thread num : 5
Mon id est : 6
Bonjour, je suis le thread num : 6
Mon id est : 7
Bonjour, je suis le thread num : 7
Mon id est : 8
Threads crees, passons a autre chose!
Bonjour, je suis le thread num : 8
Mon id est : 9
Bonjour, je suis le thread num : 9
Mon id est : 10
le thread num 0 meurs
le thread num 1 meurs
le thread num 2 meurs
le thread num 3 meurs
le thread num 4 meurs
le thread num 5 meurs
le thread num 6 meurs
le thread num 7 meurs
Allons recuperer nos valeurs de retours...
thread num 0 est termine et nous retourne la valeur : 1
thread num 1 est termine et nous retourne la valeur : 2
le thread num 8 meurs
le thread num 9 meurs
thread num 2 est termine et nous retourne la valeur : 3
thread num 3 est termine et nous retourne la valeur : 4
thread num 4 est termine et nous retourne la valeur : 5
thread num 5 est termine et nous retourne la valeur : 6
thread num 6 est termine et nous retourne la valeur : 7
thread num 7 est termine et nous retourne la valeur : 8
thread num 8 est termine et nous retourne la valeur : 9
thread num 9 est termine et nous retourne la valeur : 10
Vous remarquez qu'on a créé 10 threads qui se sont
exécutés en même temps. C'est la raison pour laquelle les messages
sont affichés dans un ordre aléatoire.
Le code ci-dessous nous permet de créer un thread et de le stocker (l'objet)
dans un tableau.
$StocageThreads
[$_
] =
threads->
create( \&
fun, $_
);
La méthode tid nous retourne le numéro id du thread.
La méthode join permet d'attendre que le thread se termine, de le nettoyer et de
retourner les valeurs de la procédure lancée dans le thread (notamment &fun
dans notre exemple). Si vous ne souhaitez pas récupérer la/les valeur(s) de retour de join,
utilisez la méthode detach qui prend moins de ressources et détache votre script
du thread. Celui sera nettoyé proprement par Perl une fois qu'il sera terminé.
Lisez les documentations de la section liens utiles,
il est très important de maîtriser un minimum les
threads pour la suite de cet article.
Il est également impératif de comprendre comment partager des données entre le
script Perl et ses threads, c'est important pour la suite. Nous utilisons le
module threads::shared.
Voici un exemple provenant du site enstimac, suivi
de ces explications (lien dans la section liens utiles).
use threads;
use threads::shared;
my $toto
: shared =
1
;
my $tata
=
1
;
threads->
new(sub {
$toto
++
; $tata
++
}
)->
join
;
print
"
$toto\n
"
; # affiche 2 car $toto est partagé
print
"
$tata\n
"
; # affiche 1 car $tata n'est pas partagé
Dans le cas d'un tableau partagé, tous les éléments du tableau sont partagés, et pour une table de hachage partagée, toutes les clés et les valeurs sont partagées. Cela place des restrictions sur ce qui peut être affecté à des éléments de tableaux et de tables de hachage partagés : seules des valeurs simples ou des références à des variables partagées sont autorisées - de façon à ce qu'une variable privée ne puisse accidentellement devenir partagée. Une affectation incorrecte entraîne la mort du thread (die). Par exemple :
use threads;
use threads::shared;
my $var
=
1
;
my $svar
: shared =
2
;
my %hash
: shared;
... créer quelques threads ...
$hash
{
a}
=
1
; # pour tous les threads, exists($hash{a}) et $hash{a} == 1
$hash
{
a}
=
$var
; # ok - copie par valeur : même effet que précédemment
$hash
{
a}
=
$svar
; # ok - copie par valeur : même effet que précédemment
$hash
{
a}
=
\$svar
; # ok - référence à une variable partagée
$hash
{
a}
=
\$var
; # entraîne la terminaison (I<die>)
delete
$hash
{
a}
; # ok - pour tous les threads, !exists($hash{a})
Le but de cet article n'est pas de vous faire un cours sur Perl et les threads,
mais de vous exposer une méthode pour utiliser les threads avec Perl Tk.
Lisez donc les documentations CPAN et cours enstimac pour une meilleure compréhension.
Pour la suite de cet article, je considère que vous avez de bonnes notions sur
les modules utilisés. De toute façon, les codes seront expliqués afin
que vous puissiez les adapter à vos besoins.
Le script est ExempleThreads (téléchargeable dans la section
"Téléchargement des scripts").
V-3. Perl Tk et les threads▲
3-a. Avantages et inconvénients▲
- L'utilisateur peut continuer à interagir avec l'interface Perl Tk pendant qu'une tâche s'effectue.
- La fenêtre Perl Tk n'est plus figée car la tâche s'effectue dans un autre processus.
- Il est possible de partager des données entre le script et les threads.
- La version actuelle de Perl/Tk (Tk-804.028) n'est pas "thread safe" d'après les auteurs.
- L'utilisation des threads avec Perl Tk n'est pas simple.
- Le partage des données entres processus légers et/ou script principal n'est pas toujours évident.
- Il est recommandé de créer ses threads avant tout code TK et ne pas faire apparaître de code TK dans les threads.
- On ne peut donc pas créer des threads à la volée comme bon nous semble via un click bouton.
Parmi les inconvénients de Perl Tk et des threads, ayez surtout conscience des pièges même des threads, ex :
- Les threads peuvent modifier l'état du processus complet, affectant ainsi les autres threads
- chdir dans un thread modifie le répertoire courant des autres threads et du script principal (excepté sous Windows)
- Lisez la documentation BUGS AND LIMITATIONS de la documentation CPAN du module threads et threads::shared
Il est important de ne pas être surpris d'un mauvais comportement de votre script à cause d'une mauvaise maîtrise des modules threads::* !
3-b. Erreurs courantes▲
Pour vous montrer l'erreur classique que l'on est amenée à faire la première fois que l'on souhaite utiliser les threads avec TK, on reprend notre exercice sur le listing des fichiers. On n'utilisera pas de barre de progression avec nos threads.
#!/usr/bin/perl
#==========================================================================
# Auteur : djibril
# But : Script Perl/Tk pour afficher l'heure, lister un répertoire
# ATTENTION : A ne pas faire
#==========================================================================
use warnings;
use strict;
use Tk;
use threads;
# Creation du widget principal
my $WidgetPrincipal
=
new MainWindow(
-
title =>
"Tk, Thread ou ProgressBar"
,
-
background =>
"white"
,
);
$WidgetPrincipal-
>
minsize( 800
, 200
);
my $Message
=
"rien"
;
my $Heure
=
"L'heure"
;
my $FormatHeure
=
0
;
# Label qui affichera l'heure
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Heure
,
-
background =>
"white"
,
)->
pack
( qw / -fill both -expand 1/
);
# Frame pour les boutons
my $FrameBouton
=
$WidgetPrincipal-
>
Frame(
-
background =>
"white"
,
)->
pack
();
$FrameBouton-
>
Button(
-
text =>
"Fermer"
,
-
command =>
sub {
exit; }
,
)->
grid( -
row =>
0
, -
column =>
0
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
$FrameBouton-
>
Button(
-
text =>
"Changer format de l'heure"
,
-
command =>
sub {
if ( $FormatHeure
==
0
) {
$FormatHeure
=
1
;
}
else {
$FormatHeure
=
0
;
}
}
,
)->
grid( -
row =>
0
, -
column =>
1
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
$FrameBouton-
>
Button(
-
text =>
"Lister des fichiers"
,
-
command =>
sub {
# Selectionner un répertoire
my $Directory
=
$WidgetPrincipal-
>
chooseDirectory(
-
title =>
"Selection d'un repertoire"
,
-
mustexist =>
1
,
);
if ( $Directory
) {
# Creation du thread
$Message
=
"Creation du thread et Listings des fichiers en cours"
;
$WidgetPrincipal-
>
update;
my $Thread
=
threads->
create( \&
ListerFichiers, $Directory
);
$Thread-
>
detach();
#$Thread->join();
$Message
=
"Listings des fichiers fini"
;
$WidgetPrincipal-
>
update;
}
}
,
)->
grid( -
row =>
0
, -
column =>
2
, -
padx =>
10
, -
pady =>
10
, -
sticky =>
"nsew"
);
# Label qui affichera l'etat
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Message
,
-
background =>
"#FFE0D0"
,
-
relief =>
"groove"
,
)->
pack
( qw / -side bottom -fill x -expand 0/
);
# Pocedure pour afficher l'heure
$WidgetPrincipal-
>
repeat( 1000
, \&
AfficherHeure );
MainLoop();
sub AfficherHeure {
# heure
my ( $Second
, $Minute
, $Hour
, $Day
, $Month
,
$Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
) =
localtime
(time
);
$Month
=
$Month
+
1
;
$Year
=
$Year
+
1900
;
foreach ( $Second
, $Minute
, $Hour
, $Day
, $Month
,
$Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
) {
s/^(\d)$/0$1/
;
}
if ( $FormatHeure
==
0
) {
$Heure
=
"(
$Day-$Month-$Year
)
$Hour
"
."::"
.$Minute
."::"
.$Second
;
}
else {
$Heure
=
"
$Hour
"
."::"
.$Minute
."::"
."
$Second
(
$Day-$Month-$Year
)"
;
}
}
#======================================================
# Nombre d'arguments : 1
# Argument(s) : un répertoire ($repertoire)
# Retourne : Tableau de fichier (@fichiers)
#======================================================
sub ListerFichiers {
my ( $repertoire
) =
@_
;
my @fichiers
;
# Ouverture d'un répertoire
opendir
(my $FhRep
, $repertoire
)
or
warn
"impossible d'ouvrir le répertoire
$repertoire\n
"
and
return;
# Liste fichiers et répertoire sauf (. et ..)
my @Contenu
=
grep
{
!
/^
\.\.
?$/
}
readdir
($FhRep
);
# Fermeture du répertoire
closedir
($FhRep
);
# On récupère tous les fichiers
foreach my $nom
( @Contenu
) {
# Fichiers
if ( -
f "
$repertoire
/
$nom
"
) {
push
( @fichiers
, "
$repertoire
/
$nom
"
);
$Message
=
"
$repertoire
/
$nom
"
; # <=== modification
print
"
$repertoire
/
$nom\n
"
;
}
# Repertoires
elsif ( -
d "
$repertoire
/
$nom
"
) {
# recursivité
push
( @fichiers
, ListerFichiers("
$repertoire
/
$nom
"
) );
}
}
return @fichiers
;
}
Ce script ressemble au script ScriptBasique2. Il n'y a pas de barre de progression et on fait appel au module threads. Dans le code du bouton "Lister des fichiers", on crée notre thread et on le détache pour qu'il soit supprimé proprement par Perl à la fin de son exécution.
my $Thread
=
threads->
create( \&
ListerFichiers, $Directory
);
$Thread-
>
detach();
Quand on exécute ce script, on remarque ceci :
Utilisation de la méthode detach
- La fenêtre Tk n'est pas figée.
- L'heure est actualisée régulièrement.
- Le script s'arrête brusquement à la fin de l'exécution du thread (du listing d'un répertoire) avec un message de ce type
"Free to wrong pool 2ccee28 not 235e40 at C:/Perl/site/lib/Tk/Widget.pm line 98 during global destruction.".
Utilisation de la méthode join à la place de detach
- La fenêtre Tk reste figée tant que le thread n'est pas terminé (ce qui est bien dommage) !
- Si on ajoute un update dans la procédure "ListerFichiers", on a un message d'erreur de ce type
Attempt to free non-existent shared string '_TK_RESULT_', Perl interpreter: 0x2ccc244 at ... - Une fois le thread terminé, le script s'arrête anormalement avec les mêmes messages d'erreurs cités ci-dessus
Pourquoi ces arrêts brusques du script ?
En fait, nous violons les règles actuelles de Perl/Tk car il n'est
pas "thread safe".
On ne doit absolument pas mettre de code Perl Tk dans une procédure lancée dans un thread.
Or, c'est le cas ici, puisque l'on fait un update
dans "ListerFichiers".
Ne me demandez pas pourquoi et quelles sont ces règles !!
Le README du module nous dit ceci :
Tk804.027 builds and loads into a threaded perl but is NOT yet thread safe.
Les auteurs et personnes en charge de la maintenance de Perl/Tk
ont prévu de le rendre "thread-safe" dans leur
ToDotodo tk
dans un futur proche !!
En attendant ces nouveautés, on va utiliser un autre procédé qui est recommandé et plus sûr.
Le script est ThreadErreurClassique (téléchargeable dans la section
"Téléchargement des scripts").
V-4. Mise en place des threads dans notre exemple▲
J'espère que vous n'êtes pas fatigué !! Après tous ces paragraphes et
exemples de codes, nous allons enfin voir comment créer proprement des
threads en Perl/Tk !
Vous devez vous mettre en tête ceci :
- On doit créer nos threads en début de script avant même d'écrire du code Tk.
- On ne doit pas faire appel à du code Perl Tk dans les procédures que l'on souhaite utiliser dans nos threads.
Je vais vous exposer le concept de notre script.
Nous allons créer un thread qui tournera en tâche de fond.
Son but sera de dormir si on ne lui demande rien
ou de travailler si on le met à contribution.
Pour lui dire de travailler, on lui enverra un signal
qu'il interceptera. Ce signal mentionne la procédure à appeler
et on récupére le résultat de notre procédure.
Pour commencer, faisons appel aux modules dont on aura besoin.
#!/usr/bin/perl
#==========================================================================
# Auteur : djibril
# But : Script Perl/Tk pour afficher l'heure, lister un répertoire
# en utilisant des threads de façon propre
#==========================================================================
use warnings;
use strict;
use Tk; # Pour créer notre GUI
use threads; # Pour créer nos threads
use threads::shared; # Pour partager nos données entre threads
use Time::HiRes qw( sleep )
; # Pour faire des sleeps < à une seconde
Créons un hash dans lequel on mentionne les fonctions à appeler dans notre thread. Ce hash a en clé le nom de la fonction et en valeur la référence à la procédure.
# Contient les fonctions à appeler dans le thread si besoin
my %LesFonctionsALancerDansThread
=
( "ListerFichiers"
=>
\&
ListerFichiers, );
Ce hash est visible dans notre thread car il a été déclaré avant même
la création du thread.
Déclarons maintenant les variables qui seront partagées entre le thread
et le thread principal (le script) :
#===================================
# Threads et variables partagées
#==================================
my $TuerThread
: shared; # Permet de tuer le thread proprement
my $NomFonction
: shared; # Contient le nom de la fonction à appeler
my $ThreadTravail
: shared; # Contient la valeur permettant au thread de lancer une procédure
my @ArgumentsThread
: shared; # Contient les arguements à passer à une éventuelle procédure
my @ResultatFonction
: shared; # Contient le résultat des fonctions lancées dans le thread
$ThreadTravail
=
0
; # 0 : thread ne fait rien, 1 : il bosse
$TuerThread
=
0
; # 0 : thread en vie, 1 : thread se termine
Nous avons choisi de partager 5 variables.
$TuerThread contient la valeur 0 ou 1.
C'est ainsi que l'on demande au thread de mourir ou non.
$NomFonction contient le nom de la fonction que l'on souhaite
appeler dans notre thread (grâce aux hash %LesFonctionsALancerDansThread).
$ThreadTravail contient la valeur 0 ou 1. C'est ainsi que l'on demande au thread
de lancer une procédure ou de dormir.
@ArgumentsThread contient les arguments que l'on souhaite
passer aux procédures lancées dans le thread.
@ResultatFonction contient les résultats de la procédure lancée dans le thread.
Ne vous inquiétez pas si cela reste ambigu pour l'instant, tout sera clarifié avec la suite
du code!
Créons maintenant notre thread.
# Création du thread
my $Thread
=
threads->
create( \&
NotreThreadUnique );
Il reste la procédure NotreThreadUnique
à concevoir !
#================================================
# NotreThreadUnique
#================================================
sub NotreThreadUnique {
# Tourne en rond
while (1
) {
# demande au thread de travailler
if ( $ThreadTravail
==
1
) {
# Lance la procédure
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
# demande au thread de dormir
$ThreadTravail
=
0
;
}
# Terminer le thread
last if ( $TuerThread
==
1
);
sleep
0
.5
;
}
return;
}
Explication :
Dans notre procédure, nous avons fait une boucle while infinie qui permet au thread
de ne jamais mourir sauf si on le lui demande.
Dans un premier temps, le thread vérifie si la variable
$ThreadTravail est à 1.
Si c'est le cas, cela signifie qu'on a demandé au thread de
lancer une procédure (on verra plus tard comment on
s'y prend).
Dans le cas contraire, on vérifie si le thread doit mourir ou dormir
pendant une demie seconde.
Maintenant que nos variables sont déclarées et partagées, et notre thread
créé, passons au Perl Tk.
#===================================
# Debut du code principal Perl Tk
#==================================
# Creation du widget principal
my $WidgetPrincipal
=
new MainWindow(
-
title =>
"Perl/Tk et les threads"
,
-
background =>
"white"
,
);
$WidgetPrincipal-
>
minsize( 800
, 200
);
my $Message
=
"rien"
; # Texte qui changera dans le label
my $Heure
=
"L'heure"
; # Heure à afficher
my $FormatHeure
=
0
; # Format de l'heure
# Label qui affichera l'heure
my $LabelHeure
=
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Heure
,
-
background =>
"white"
,
)->
pack
(qw / -fill both -expand 1/
);
# Frame pour les boutons
my $FrameBouton
=
$WidgetPrincipal-
>
Frame( -
background =>
"white"
, )->
pack
();
my $BoutonFermer
=
$FrameBouton-
>
Button(
-
text =>
"Fermer"
,
-
command =>
[ \&
Fermer, $Thread
],
)->
grid(
-
row =>
0
,
-
column =>
0
,
-
padx =>
10
,
-
pady =>
10
,
-
sticky =>
"nsew"
);
my $BoutonChangerFormatHeure
=
$FrameBouton-
>
Button(
-
text =>
"Changer format de l'heure"
,
-
command =>
sub {
if ( $FormatHeure
==
0
) {
$FormatHeure
=
1
;
}
else {
$FormatHeure
=
0
;
}
}
,
)->
grid(
-
row =>
0
,
-
column =>
1
,
-
padx =>
10
,
-
pady =>
10
,
-
sticky =>
"nsew"
);
my $BoutonListerFichier
=
$FrameBouton-
>
Button(
-
text =>
"Lister des fichiers"
,
-
command =>
\&
ListerFichiersTk,
)->
grid(
-
row =>
0
,
-
column =>
2
,
-
padx =>
10
,
-
pady =>
10
,
-
sticky =>
"nsew"
);
# Label qui affichera l'etat
my $LabelMessage
=
$WidgetPrincipal-
>
Label(
-
textvariable =>
\$Message
,
-
background =>
"#FFE0D0"
,
-
relief =>
"groove"
,
)->
pack
(qw / -side bottom -fill x -expand 0/
);
# Pocedure pour afficher l'heure
$WidgetPrincipal-
>
repeat( 1000
, \&
AfficherHeure );
MainLoop();
Nous créons notre fenêtre Tk avec une taille minimum (minsize),
puis nous déclarons 3 variables.
$Message et $Heure servent aux labels
et sont modifiées régulièrement.
$FormatHeure nous permet de définir le type
de format de l'heure à afficher.
Nous créons ensuite notre label pour l'heure et
un cadre pour accueillir les 3 boutons. Pour terminer, nous créons
un dernier label qui est en bas de la fenêtre. Nous lançons
la procédure "AfficherHeure" toutes les secondes via
la méthode Perl Tk repeat.
Le bouton "Fermer" permet de détruire le script proprement.
Il appelle la procédure "Fermer" qui demande au thread de
se terminer, puis détruit le script.
sub Fermer {
# Demande au thread de se terminer
$TuerThread
=
1
;
# On attends que le thread se termine proprement
$Thread-
>
detach();
exit;
return;
}
Le bouton "Changer format de l'heure" permet juste de positionner la variable $FormatHeure à 1 ou à 0. Comme la procédure "AfficherHeure" est lancée toutes les secondes, en fonction de $FormatHeure, l'affichage sera différent.
sub AfficherHeure {
# récupérons la date et l'heure
my ( $Second
, $Minute
, $Hour
, $Day
, $Month
, $Year
, $DayWeek
, $DayYear
, $HourWinterOrSummer
)
=
localtime
(time
);
$Month
=
$Month
+
1
;
$Year
=
$Year
+
1900
;
foreach ( $Second
, $Minute
, $Hour
, $Day
, $Month
) {
s/^(\d)$/0$1/
;
}
# format 1
if ( $FormatHeure
==
0
) {
$Heure
=
"(
$Day-$Month-$Year
)
$Hour
"
. "::"
. $Minute
. "::"
. $Second
;
}
# format 2
else {
$Heure
=
"
$Hour
"
. "::"
. $Minute
. "::"
. "
$Second
(
$Day-$Month-$Year
)"
;
}
return;
}
Le bouton ListerFichier permet de lancer la procédure "ListerFichiersTk". Elle nous permet de lister un répertoire choisi par l'utilisateur dans le thread.
sub ListerFichiersTk {
# Selectionner un répertoire
my $Directory
=
$WidgetPrincipal-
>
chooseDirectory(
-
title =>
"Selection d'un repertoire"
,
-
mustexist =>
1
,
);
if ($Directory
) {
$Message
=
"Listings des fichiers en cours"
;
# On va demander au thread de bosser
$NomFonction
=
"ListerFichiers"
; # On lui indique la procédure à appeler
@ArgumentsThread
=
($Directory
); # On lui donne les arguments
$ThreadTravail
=
1
; # On lui demande de bosser
}
return;
}
Dans la procédure ci-dessus, on demande d'abord à l'utilisateur de choisir un répertoire. S'il en choisit un, on fait appel à notre thread pour travailler. On indique alors dans notre variable partagée le nom de la procédure à appeler.
$NomFonction
=
"ListerFichiers"
;
On fait de même avec les bons arguments :
@ArgumentsThread
=
($Directory
);
@ArgumentsThread contient un seul élément, mais il aurait
pu en contenir plusieurs si notre procédure "ListerFichiers"
en attendait plus. Une fois qu'il a fini de
lister les fichiers, il se re-endort.
Le script complet est PerlTkThreadPropreFinal1
(téléchargeable dans la section
"Téléchargement des scripts").
A ce stade, voici quelques remarques :
- Lorsque l'on clique sur le bouton "lister fichiers", la fenêtre n'est plus figée.
- L'heure s'affiche normalement et régulièrement.
- Le thread ne bug plus et ne s'arrête pas de façon brusque.
- A la fermeture du script, on fait appel à la méthode detach sans souci. On aurait pu également appeler la méthode join.
- Pour le moment, on ne sait pas comment récupérer le résultat de la procédure lancée dans le thread.
- Dans le script principal, on ne sait pas concrètement quand le thread est terminé.
- L'utilisateur peut cliquer sur le bouton "lister fichiers" alors que le répertoire est encore en cours de listing.
On a quand même déjà beaucoup d'avantages par rapport aux
inconvénients, non ? Nous allons maintenant voir
comment on peut améliorer notre script.
Il est important que vous soyez bien familier avec les notions de références en Perl
pour cette partie de l'article.
Pour récupérer les résultats des procédures lancées dans notre thread, on a
prévu une variable partagée.
my @ResultatFonction
: shared; # Contient le résultat des fonctions lancées dans le thread
Nous allons l'utiliser dans notre procédure "NotreThreadUnique" en modifiant la ligne de code suivante :
# Lance la procédure
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
en celle-ci
# Lance la procédure et récupère le résultat
my @Resultat
=
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
On peut ainsi dans un premier temps récupérer le résultat retourné par la procédure lancée. Vous allez sûrement vous demander pourquoi on n'a pas tout simplement écrit :
# Lance la procédure et récupère le résultat
my @ResultatFonction
=
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
Si vous mettez directement le résultat dans @ResultatFonction, le code sera bon
tant que votre procédure ne retourne que des scalaires, un tableau de scalaires partagés ou
des références de tableaux (ou hash) partagées. Ceci est bien expliqué dans la documentation
du module threads::shared.
Si ce n'est pas le cas, vous obtiendrez
un message d'erreurs du type Thread 1 terminated abnormally:
Invalid value for shared scalar at ... et le thread s'arrêtera.
Rien de mieux qu'un exemple!
Supposons que notre procédure "ListerFichiers" ne retourne
pas @fichiers comme c'est le cas pour l'instant, mais plutôt une référence
de @fichiers.
Si on écrit
# Lance la procédure et récupère le résultat
my @ResultatFonction
=
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
@ResultatFonction contient une référence à un tableau qui n'est pas partagé.
Cela va génére l'erreur Thread 1 terminated abnormally:
Invalid value for shared scalar at .... Il faut donc utiliser la
méthode shared_clone qui prend en argument une référence de
tableau ou hash et copie tous les éléments non partagés.
NB : La méthode shared_clone retourne une réference
de hash ou de tableau.
my $RefHash
=
shared_clone( \@ARRAY
); # => retourne une référence de tableau
my $RefARRAY
=
shared_clone( \%HASH
); # => retourne une référence de hash
Dans tous les cas, on utilise la méthode shared_clone. Comme elle nous retourne une référence, on va dans plutôt partager une variable s'appelant $RefResultatFonction à la place de @ResultatFonction.
#================================================
...
my $RefResultatFonction
: shared; # Contient le résultat des fonctions lancées dans le thread
...
...
# NotreThreadUnique
#================================================
sub NotreThreadUnique {
# Tourne en rond
while (1
) {
# demande au thread de travailler
if ( $ThreadTravail
==
1
) {
# Lance la procédure et récupère le résultat
my @Resultat
=
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
$RefResultatFonction
=
shared_clone( \@Resultat
);
# demande au thread de dormir
$ThreadTravail
=
0
;
}
# Terminer le thread
last if ( $TuerThread
==
1
);
sleep
0
.5
;
}
return;
}
Notre variable contient maintenant à chaque fois le résultat de
notre procédure.
Nous déterminons le moment où la procédure
lancée par thread est terminée et affichons le résultat.
On empêche également un autre click sur le bouton "Lister fichiers"
tant que le listing est en cours.
Modifions notre procédure "ListerFichiersTk" :
sub ListerFichiersTk {
# Selectionner un répertoire
my $Directory
=
$WidgetPrincipal-
>
chooseDirectory(
-
title =>
"Selection d'un repertoire"
,
-
mustexist =>
1
,
);
if ($Directory
) {
$Message
=
"Listings des fichiers en cours"
;
# On va demander au thread de bosser
$NomFonction
=
"ListerFichiers"
; # On lui indique la procédure à appeler
@ArgumentsThread
=
( $Directory
); # On lui donne les arguments
$ThreadTravail
=
1
; # On lui demande de bosser
# ===> Modification (Rajout) <=============
# On desactive le bouton ListerFichiers
$BoutonListerFichier-
>
configure(-
state =>
"disabled"
);
# Tant que le thread travail, on attends
while ( $ThreadTravail
==
1
) {
sleep
0
.2
;
$WidgetPrincipal-
>
update;
}
$Message
=
"Listings des fichiers fini"
;
# On réactive le bouton ListerFichiers
$BoutonListerFichier-
>
configure(-
state =>
"normal"
);
# On affiche le resulat
my $NbrFichier
=
scalar
( @
{
$RefResultatFonction
}
);
print
"Nombre de fichiers :
$NbrFichier\n
"
;
foreach my $Fichier
( @
{
$RefResultatFonction
}
) {
print
"-
$Fichier\n
"
;
}
}
return;
}
Explication :
Une fois que le thread se met à travailler, on désactive le bouton
"Lister fichiers". On effectue ensuite une boucle while qui
vérifie toutes les 200 millisecondes s'il a terminé ou non .
On update le widget, sinon ce dernier sera figé.
On teste si $ThreadTravail est égal à 1 ou non.
A chaque fois que le thread a fini de travailler, il se re-endort,
$ThreadTravail = 0 ) !
...
# Lance la procédure et récupère le résultat
my @Resultat
=
$LesFonctionsALancerDansThread
{
$NomFonction
}->
(@ArgumentsThread
);
$RefResultatFonction
=
shared_clone( \@Resultat
);
# demande au thread de dormir
$ThreadTravail
=
0
; #<======= Dort mon ami thread
...
Une fois que le thread est endormi, on peut réactiver le bouton et lire le résultat.
# On affiche le resulat
my $NbrFichier
=
scalar
( @
{
$RefResultatFonction
}
);
print
"Nombre de fichiers :
$NbrFichier\n
"
;
foreach my $Fichier
( @
{
$RefResultatFonction
}
) {
print
"-
$Fichier\n
"
;
}
Dans notre cas, $RefResultatFonction est une référence de tableau,
on en tient compte pour extraire les résultats.
Le script est PerlTkThreadPropreFinal (téléchargeable dans la section
"Téléchargement des scripts").
On a enfin terminé! Notre script fonctionne maintenant correctement :
- La fenêtre n'est plus figée et l'heure s'affiche de manière régulière.
- L'utilisateur ne peut pas cliquer sur le bouton lorsque le listing est en cours.
- On récupère proprement les résultats de notre listing de fichiers.
- Le thread ne s'arrête plus brusquement.
- A la fermeture de la fenêtre Perl Tk, le thread est proprement détruit.
Voilà, vous savez maintenant comment utiliser les threads avec Perl Tk! Vous pouvez vous inspirer de ces scripts : PerlTkThreadPropreFinal pour l'utilisation des threads et ScriptProgressBar pour l'utilisation des barres de progression. Adaptez les à vos besoins! Si vous voulez utiliser un seul thread pour pouvoir lancer diverses procédures, il vous suffit de modifier le hash %LesFonctionsALancerDansThread.
my %LesFonctionsALancerDansThread
=
(
"ListerFichiers"
=>
\&
ListerFichiers,
"ZiperUnRepertoire"
=>
\&
ZiperUnRepertoire,
"CalculTresLong"
=>
\&
CalculTresLong,
"CalculTresTresLong"
=>
\&
CalculTresTresLong,
...
);
Dans notre exemple, on a attend que le thread se re-endorme
pour poursuivre le script principal, mais ce n'est pas une obligation.
Tout dépend de ce que vous voulez faire.
Il faut juste faire attention "à ne pas se mélanger les pinceaux"
et ne pas écraser les données par erreur.
Il est possible de faire des choses plus complexes, tout est fonction
de votre imagination et votre cahier des charges.
On a également décidé d'utiliser un thread, mais vous auriez pu
en utiliser plusieurs, c'est toujours le même principe. A vous de bien définir ce
que vous souhaitez, à penser à protéger les variables partagées si nécessaire via la
méthode lock (du module threads::shared).
Pour conclure, voici quelques inconvénients à l'utilisation des
threads avec Perl Tk (et oui, il y en a quand même)!
- Vous avez pu remarquer qu'on est obligé de les créer en début de script.
Si on choisit d'en créer un seul, il nous sera impossible d'en
créer d'autres par la suite.
- Si pour différentes raisons, la procédure lancée par votre thread produit un die,
votre thread sera détruit. Si vous n'en aviez qu'un, votre script ne pourra
donc plus fonctionner correctement.
- Il peut être important de tester que nos threads sont toujours en vie.
- Pour finir, vous avez pu constater que l'utilisation des threads en Perl Tk
n'est pas très évidente, il faut se creuser les méninges
!!!
Quoi qu'il en soit, un bon algorithme est nécessaire pour utiliser
au mieux les threads et construire une application Perl Tk puissante
.
VI. Téléchargement des scripts▲
Vous pouvez télécharger tous les scripts de cet article iciScripts de cet article.
VII. Conclusion▲
Dans cet article, nous avons abordé 2 notions très utiles en Perl Tk :
les barres de progression et les threads.
Elle permettent comme vous avez pu le constater, de ne pas figer les widgets,
de montrer l'évolution d'un calcul ou d'une tâche à un utilisateur,
de faire plusieurs choses en même temps si on le souhaite.
Voici quelques conseils dans le choix d'utiliser une barre de progression,
les threads, ou même les deux en Perl Tk :
- optez en priorité pour le choix d'une barre de progression si possible.
Cela vous permet de gérer facilement vos procédures, votre script en Perl Tk
en bénéficiant des méthodes Perl/Tk disponibles
(update, repeat, Busy, Unbusy, ...).
- en dernier recours, si vous n'avez vraiment pas le choix car vous
utilisez un module externe pour effectuer de longs calculs, alors
pensez aux threads. Gardez à l'esprit qu'en cas de "die"
dans un thread, ce dernier meurt. De plus, il faut bien réfléchir
au nombre de threads à créer en début de script, à la façon dont
on souhaite protéger les données partagées, etc.
Maintenant que vous êtes bien armé, à vous de jouer !!
N'hésitez pas à faire des appréciations, suggestions, remarques ou corrections
au sujet de cet article
icisujet article Perl/TK et les threads ou barre de progressions
.
VIII. Liens utiles▲
IX. Remerciements▲
Je remercie stoyakprofil stoyak et l'équipe Perl de dvp.com pour la relecture.