Procédure pas à pas : création d'un composant simple multithread à l'aide de Visual C#
Mise à jour : novembre 2007
Le composant BackgroundWorker remplace l'espace de noms System.Threading et lui ajoute des fonctionnalités ; toutefois, l'espace de noms System.Threading est conservé pour la compatibilité descendante et une utilisation ultérieure, si tel est votre choix. Pour plus d'informations, consultez Vue d'ensemble du composant BackgroundWorker.
Vous pouvez écrire des applications capables d'effectuer plusieurs tâches simultanément. Cette possibilité, appelée modèle multithread ou modèle de thread libre, est un moyen puissant de concevoir des composants sollicitant beaucoup le processeur et nécessitant une entrée d'utilisateur. Par exemple, un composant calculant des informations relatives aux salaires peut utiliser le multithreading. Ce composant peut traiter les données entrées dans une base de données par un utilisateur sur un thread tout en effectuant les calculs des salaires, qui sollicitent beaucoup le processeur, sur un autre thread. Grâce à l'exécution de ces processus sur des threads distincts, les utilisateurs n'ont pas besoin d'attendre que l'ordinateur ait terminé les calculs pour entrer de nouvelles données. Dans cette procédure pas à pas, vous allez créer un composant multithread simple qui effectue simultanément plusieurs calculs complexes.
Création du projet
L'application est constituée d'un formulaire unique et d'un composant. L'utilisateur entre des valeurs et indique au composant qu'il peut commencer les calculs. Le formulaire reçoit ensuite des valeurs du composant et les affiche dans des contrôles Label. Le composant exécute les calculs sollicitant le processeur et indique au formulaire la fin de ces calculs. Vous allez créer des variables publiques dans votre composant pour y stocker les valeurs reçues de l'interface utilisateur. Vous allez également implémenter des méthodes dans votre composant afin d'effectuer les calculs en fonction des valeurs de ces variables.
Remarque : |
---|
Bien qu'il soit généralement préférable d'utiliser une fonction pour une méthode qui calcule une valeur, il n'est pas possible de passer les arguments entre threads, ni de retourner des valeurs. Il existe plusieurs moyens simples de fournir des valeurs aux threads et de recevoir des valeurs de ces threads. Dans cette démonstration, vous allez retourner des valeurs à l'interface utilisateur en mettant à jour des variables publiques ; des événements seront utilisés pour informer le programme principal de la fin de l'exécution d'un thread. Selon vos paramètres actifs ou votre édition, les boîtes de dialogue et les commandes de menu que vous voyez peuvent différer de celles qui sont décrites dans l'aide. Pour modifier vos paramètres, choisissez Importation et exportation de paramètres dans le menu Outils. Pour plus d'informations, consultez Paramètres Visual Studio. |
Pour créer le formulaire
Créez un nouveau projet Application Windows.
Nommez l'application Calculations et renommez Form1.cs comme frmCalculations.cs. Lorsque Visual Studio vous invite à renommer l'élément de code Form1, cliquez sur Oui.
Ce formulaire servira d'interface utilisateur principale pour votre application.
Ajoutez à votre formulaire cinq contrôles Label, quatre contrôles Button et un contrôle TextBox.
Définissez les propriétés de ces contrôles comme suit :
Contrôle
Nom
Texte
label1
lblFactorial1
(vide)
label2
lblFactorial2
(vide)
label3
lblAddTwo
(vide)
label4
lblRunLoops
(vide)
label5
lblTotalCalculations
(vide)
button1
btnFactorial1
Factorial
button2
btnFactorial2
Factorial - 1
button3
btnAddTwo
Add Two
button4
btnRunLoops
Run a Loop
textBox1
txtValue
(vide)
Pour créer le composant Calculator
Dans le menu Projet, choisissez Ajouter un composant.
Nommez le composant Calculator.
Pour ajouter des variables publiques au composant Calculator
Ouvrez l'éditeur de code pour Calculator.
Ajoutez des instructions pour créer des variables publiques qui seront utilisées pour passer les valeurs de frmCalculations à chaque thread.
La variable varTotalCalculations conserve en permanence le total du nombre de calculs effectués par le composant, l'autre variable recevant les valeurs du formulaire.
public int varAddTwo; public int varFact1; public int varFact2; public int varLoopValue; public double varTotalCalculations = 0;
Pour ajouter des méthodes et des événements au composant Calculator
Déclarez les délégués pour les événements qui seront utilisés par votre composant pour communiquer les valeurs au formulaire.
Remarque : Bien que vous déclariez quatre événements, vous n'avez besoin de créer que trois délégués, car deux événements comporteront la même signature.
Tapez le code suivant juste en dessous des déclarations de variable entrées lors de l'étape précédente :
// This delegate will be invoked with two of your events. public delegate void FactorialCompleteHandler(double Factorial, double TotalCalculations); public delegate void AddTwoCompleteHandler(int Result, double TotalCalculations); public delegate void LoopCompleteHandler(double TotalCalculations, int Counter);
Déclarez les événements que votre composant utilisera pour communiquer avec votre application. Pour ce faire, ajoutez le code suivant, juste en dessous du code entré lors de l'étape précédente.
public event FactorialCompleteHandler FactorialComplete; public event FactorialCompleteHandler FactorialMinusOneComplete; public event AddTwoCompleteHandler AddTwoComplete; public event LoopCompleteHandler LoopComplete;
Tapez le code suivant juste en dessous du code que vous avez tapé lors de l'étape précédente :
// This method will calculate the value of a number minus 1 factorial // (varFact2-1!). public void FactorialMinusOne() { double varTotalAsOfNow = 0; double varResult = 1; // Performs a factorial calculation on varFact2 - 1. for (int varX = 1; varX <= varFact2 - 1; varX++) { varResult *= varX; // Increments varTotalCalculations and keeps track of the current // total as of this instant. varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } // Signals that the method has completed, and communicates the // result and a value of total calculations performed up to this // point. FactorialMinusOneComplete(varResult, varTotalAsOfNow); } // This method will calculate the value of a number factorial. // (varFact1!) public void Factorial() { double varResult = 1; double varTotalAsOfNow = 0; for (int varX = 1; varX <= varFact1; varX++) { varResult *= varX; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } FactorialComplete(varResult, varTotalAsOfNow); } // This method will add two to a number (varAddTwo+2). public void AddTwo() { double varTotalAsOfNow = 0; int varResult = varAddTwo + 2; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; AddTwoComplete(varResult, varTotalAsOfNow); } // This method will run a loop with a nested loop varLoopValue times. public void RunALoop() { int varX; double varTotalAsOfNow = 0; for (varX = 1; varX <= varLoopValue; varX++) { // This nested loop is added solely for the purpose of slowing down // the program and creating a processor-intensive application. for (int varY = 1; varY <= 500; varY++) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } } LoopComplete(varTotalAsOfNow, varLoopValue); }
Transfert de l'entrée d'utilisateur vers le composant
L'étape suivante consiste à ajouter du code à frmCalculations afin de recevoir des entrées de l'utilisateur et d'échanger des valeurs avec le composant Calculator.
Pour implémenter des fonctionnalités client dans frmCalculations
Ouvrez frmCalculations dans l'éditeur de code.
Recherchez l'instruction public partial class frmCalculations. Juste en dessous de l'accolade ouvrante { tapez :
Calculator Calculator1;
Recherchez le constructeur. Juste avant l'accolade fermante }, ajoutez la ligne suivante :
// Creates a new instance of Calculator. Calculator1 = new Calculator();
Dans le concepteur, cliquez sur chacun des boutons afin de générer la structure du code pour les Click de chaque contrôle et d'ajouter le code permettant de créer les gestionnaires.
Une fois cette opération terminée, les gestionnaires d'événements de type Click doivent ressembler au code suivant :
// Passes the value typed in the txtValue to Calculator.varFact1. private void btnFactorial1_Click(object sender, System.EventArgs e) { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = false; Calculator1.Factorial(); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; Calculator1.FactorialMinusOne(); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; Calculator1.AddTwo(); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; Calculator1.RunALoop(); }
Après le code que vous avez ajouté lors de l'étape précédente, tapez le code suivant afin de gérer les événements que votre formulaire recevra de Calculator1 :
private void FactorialHandler(double Value, double Calculations) // Displays the returned value in the appropriate label. { lblFactorial1.Text = Value.ToString(); // Re-enables the button so it can be used again. btnFactorial1.Enabled = true; // Updates the label that displays the total calculations performed lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void FactorialMinusHandler(double Value, double Calculations) { lblFactorial2.Text = Value.ToString(); btnFactorial2.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void AddTwoHandler(int Value, double Calculations) { lblAddTwo.Text = Value.ToString(); btnAddTwo.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void LoopDoneHandler(double Calculations, int Count) { btnRunLoops.Enabled = true; lblRunLoops.Text = Count.ToString(); lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); }
Dans le constructeur de frmCalculations, ajoutez le code suivant immédiatement avant } afin de traiter les événements personnalisés que le formulaire recevra de Calculator1.
Calculator1.FactorialComplete += new Calculator.FactorialCompleteHandler(this.FactorialHandler); Calculator1.FactorialMinusOneComplete += new Calculator.FactorialCompleteHandler(this.FactorialMinusHandler); Calculator1.AddTwoComplete += new Calculator.AddTwoCompleteHandler(this.AddTwoHandler); Calculator1.LoopComplete += new Calculator.LoopCompleteHandler(this.LoopDoneHandler);
Test de l'application
Vous venez de créer un projet qui intègre un formulaire et un composant capable d'effectuer plusieurs calculs complexes. Bien que vous n'ayez pas encore implémenté la fonctionnalité de multithreading, vous allez tester votre projet pour vérifier qu'il fonctionne, avant de continuer.
Pour tester le projet
Dans le menu Déboguer, cliquez sur Démarrer le débogage.
L'application démarre et frmCalculations apparaît.
Dans la zone de texte, tapez 4, puis cliquez sur le bouton intitulé Add Two.
Le chiffre "6" doit s'afficher dans l'étiquette située sous ce bouton et le texte "Total Calculations are 1" doit s'afficher dans lblTotalCalculations.
Cliquez à présent sur le bouton intitulé Factorial - 1.
Le chiffre "6" doit s'afficher dans l'étiquette située sous le bouton ; lblTotalCalculations affiche à présent "Total Calculations are 4".
Remplacez la valeur de la zone de texte par 20, puis cliquez sur le bouton intitulé Factorial.
Le nombre "2.43290200817664E+18" s'affiche dans l'étiquette située sous ce bouton ; lblTotalCalculations affiche à présent "Total Calculations are 24".
Remplacez la valeur de la zone de texte par 50 000, puis cliquez sur le bouton Run A Loop.
Remarquez que ce bouton n'est réactivé qu'après un instant bref mais perceptible. L'étiquette située sous ce bouton doit afficher "50000" et le nombre total de calculs affiche "25000024".
Remplacez la valeur de la zone de texte par 5000000, cliquez sur le bouton intitulé Run A Loop, puis cliquez immédiatement sur le bouton intitulé Add Two. Cliquez à nouveau sur ce bouton.
Le bouton ne répond pas, pas plus que les contrôles du formulaire, tant que les boucles ne sont pas terminées.
Si votre programme exécute un seul thread d'exécution, les calculs sollicitant beaucoup le processeur, comme ceux de l'exemple ci-dessus, ont tendance à bloquer le programme jusqu'à ce qu'ils soient terminés. Dans la section suivante, vous allez ajouter des fonctionnalités de multithreading à votre application afin de permettre l'exécution simultanée de plusieurs threads.
Ajout de la fonctionnalité de multithreading
L'exemple précédent a permis d'illustrer les limites des applications n'exécutant qu'un seul thread d'exécution. Dans la section suivante, vous utiliserez la classe Thread pour ajouter plusieurs threads d'exécution à votre composant.
Pour ajouter la sous-routine Threads
Ouvrez Calculator.cs dans l'éditeur de code.
Vers le haut du code, recherchez la déclaration de classe, puis, juste en dessous de l'accolade ouvrante {, tapez ceci :
// Declares the variables you will use to hold your thread objects. public System.Threading.Thread FactorialThread; public System.Threading.Thread FactorialMinusOneThread; public System.Threading.Thread AddTwoThread; public System.Threading.Thread LoopThread;
Juste avant la fin de la déclaration de classe, en bas du code, ajoutez la méthode suivante :
public void ChooseThreads(int threadNumber) { // Determines which thread to start based on the value it receives. switch(threadNumber) { case 1: // Sets the thread using the AddressOf the subroutine where // the thread will start. FactorialThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.Factorial)); // Starts the thread. FactorialThread.Start(); break; case 2: FactorialMinusOneThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.FactorialMinusOne)); FactorialMinusOneThread.Start(); break; case 3: AddTwoThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.AddTwo)); AddTwoThread.Start(); break; case 4: LoopThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.RunALoop)); LoopThread.Start(); break; } }
Lorsqu'un Thread est instancié, il requiert un argument sous la forme d'un ThreadStart. L'objet ThreadStart est un délégué qui pointe sur l'adresse de la méthode où le thread doit commencer. Un ThreadStart ne peut pas accepter de paramètres ni passer de valeurs. Par conséquent, il ne peut indiquer qu'une méthode void. La méthode ChooseThreads que vous venez d'implémenter reçoit une valeur du programme qui l'appelle et utilise cette valeur pour déterminer le thread à démarrer.
Pour ajouter le code approprié à frmCalculations
Ouvrez le fichier frmCalculations.cs dans l'éditeur de code, puis recherchez private void btnFactorial1_Click.
Commentez la ligne appelant la méthode Calculator1.Factorial1 directement comme suit :
// Calculator1.Factorial()
Ajoutez la ligne suivante afin d'appeler la méthode Calculator1.ChooseThreads :
// Passes the value 1 to Calculator1, thus directing it to start the // correct thread. Calculator1.ChooseThreads(1);
Modifiez de la même façon les autres méthodes button_click.
Remarque : Prenez soin d'inclure la valeur appropriée pour l'argument Threads.
Lorsque vous avez terminé, votre code doit ressembler au code suivant :
private void btnFactorial1_Click(object sender, System.EventArgs e) // Passes the value typed in the txtValue to Calculator.varFact1 { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete btnFactorial1.Enabled = false; // Calculator1.Factorial(); Calculator1.ChooseThreads(1); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; // Calculator1.FactorialMinusOne(); Calculator1.ChooseThreads(2); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; // Calculator1.AddTwo(); Calculator1.ChooseThreads(3); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; // Calculator1.RunALoop(); Calculator1.ChooseThreads(4); }
Marshaling d'appels vers des contrôles
Vous allez à présent faciliter la mise à jour de l'affichage sur le formulaire. Les contrôles appartenant toujours au thread d'exécution principal, tout appel à un contrôle par un thread secondaire doit être un appel marshaling. Le marshaling consiste à déplacer un appel au-delà des limites d'un thread ; c'est une opération très coûteuse en termes de ressources. Pour réduire la quantité de marshaling nécessaire et garantir que la gestion de vos appels s'effectue en mode thread-safe, vous allez utiliser la méthode Control.BeginInvoke pour appeler des méthodes sur le thread d'exécution principal. La quantité de marshaling requise à travers des frontières de threads est ainsi réduite. Ce type d'appel est nécessaire lors de l'appel à des méthodes qui manipulent des contrôles. Pour plus d'informations, consultez Comment : manipuler des contrôles à partir de threads.
Pour créer les procédures qui appellent les contrôles
Ouvrez l'éditeur de code pour frmCalculations. Dans la section des déclarations, ajoutez le code suivant :
public delegate void FHandler(double Value, double Calculations); public delegate void A2Handler(int Value, double Calculations); public delegate void LDHandler(double Calculations, int Count);
Invokeet BeginInvoke requièrent un délégué vers la méthode appropriée en tant qu'argument. Ces lignes déclarent les signatures du délégué qui seront utilisées par BeginInvoke pour appeler les méthodes appropriées.
Ajoutez à votre code les méthodes vides suivantes :
public void FactHandler(double Value, double Calculations) { } public void Fact1Handler(double Value, double Calculations) { } public void Add2Handler(int Value, double Calculations) { } public void LDoneHandler(double Calculations, int Count) { }
Dans le menu Edition, utilisez Couper et Coller pour couper tout le code de la méthode FactorialHandler et le coller dans FactHandler.
Répétez l'étape précédente pour FactorialMinusHandler et Fact1Handler, AddTwoHandler, Add2Handler, LoopDoneHandler et LDoneHandler.
Une fois ces opérations effectuées, il ne doit rester aucun code dans FactorialHandler, Factorial1Handler, AddTwoHandler et LoopDoneHandler, et tout le code que ces méthodes utilisaient doit avoir été déplacé dans les nouvelles méthodes appropriées.
Appelez la méthode BeginInvoke pour appeler les méthodes de façon asynchrone. Vous pouvez appeler BeginInvoke à partir de votre formulaire (this) ou d'un quelconque contrôle du formulaire.
Lorsque vous avez terminé, votre code doit ressembler au code suivant :
protected void FactorialHandler(double Value, double Calculations) { // BeginInvoke causes asynchronous execution to begin at the address // specified by the delegate. Simply put, it transfers execution of // this method back to the main thread. Any parameters required by // the method contained at the delegate are wrapped in an object and // passed. this.BeginInvoke(new FHandler(FactHandler), new Object[] {Value, Calculations}); } protected void FactorialMinusHandler(double Value, double Calculations) { this.BeginInvoke(new FHandler(Fact1Handler), new Object [] {Value, Calculations}); } protected void AddTwoHandler(int Value, double Calculations) { this.BeginInvoke(new A2Handler(Add2Handler), new Object[] {Value, Calculations}); } protected void LoopDoneHandler(double Calculations, int Count) { this.BeginInvoke(new LDHandler(LDoneHandler), new Object[] {Calculations, Count}); }
En apparence, le gestionnaire d'événements effectue un simple appel à la méthode suivante. Le gestionnaire d'événements entraîne en réalité l'appel d'une méthode sur le thread principal de l'opération. Cette approche permet d'éviter les appels au-delà des limites des threads et permet l'exécution efficace des applications multithread, sans risque de blocage. Pour plus d'informations sur l'utilisation des contrôles dans un environnement multithread, consultez Comment : manipuler des contrôles à partir de threads.
Enregistrez votre travail.
Testez la solution en choisissant Démarrer le débogage dans le menu Déboguer.
Tapez 10000000 dans la zone de texte, puis cliquez sur Run A Loop.
"Looping" s'affiche dans l'étiquette située sous ce bouton. L'exécution de cette boucle doit durer un temps relativement important. Si elle se termine trop tôt, ajustez la taille du nombre en conséquence.
Cliquez rapidement sur les trois boutons toujours activés. Vous remarquez que les trois boutons répondent. L'étiquette située sous Add Two doit être la première à afficher un résultat. Les résultats s'afficheront ensuite dans les étiquettes situées en dessous des boutons Factorial. Ces résultats sont infinis, dans la mesure où le nombre retourné par une factorielle 10 000 000 est trop grand pour tenir dans une variable à double précision. Enfin, après un délai supplémentaire, les résultats sont retournés sous le bouton Run A Loop.
Comme vous venez de le voir, quatre ensembles de calculs distincts ont été effectués simultanément, sur quatre threads distincts. L'interface utilisateur a continué de répondre aux entrées et les résultats ont été retournés après l'exécution de chaque thread.
Coordination des threads
Un utilisateur expérimenté en matière d'applications multithread remarquera sans doute un subtil défaut dans le code saisi. Appelez de nouveau les lignes de code de chaque méthode de calcul dans Calculator :
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
Ces deux lignes de code incrémentent la variable publique varTotalCalculations et affectent cette valeur à la variable locale varTotalAsOfNow. Cette valeur est ensuite retournée à frmCalculations et affichée dans un contrôle de type étiquette. Cependant, la valeur retournée est-elle correcte ? Si un seul thread s'exécute, la réponse est clairement oui. En revanche, la réponse est plus incertaine dès lors que plusieurs threads s'exécutent. Chaque thread peut incrémenter la variable varTotalCalculations. Il est possible qu'après l'incrémentation de cette variable par un thread, un autre thread ait modifié cette variable en l'incrémentant avant que le premier thread n'ait copié la valeur dans varTotalAsOfNow. D'où la possibilité de voir chaque thread rapporter, en fait, des résultats inexacts. Visual C# fournit le lock, instruction (Référence C#) pour permettre la synchronisation des threads afin que chaque thread retourne toujours un résultat exact. La syntaxe de lock est la suivante :
lock(AnObject)
{
// Insert code that affects the object.
// Insert more code that affects the object.
// Insert more code that affects the object.
// Release the lock.
}
Lors de l'entrée dans le bloc lock, l'exécution sur l'expression spécifiée est bloquée aussi longtemps que le thread spécifié dispose d'un verrou exclusif sur l'objet en question. Dans l'exemple ci-dessus, l'exécution est bloquée sur AnObject. lock doit être utilisé avec un objet qui retourne une référence et non une valeur. L'exécution peut ensuite se poursuivre en tant que bloc, sans interférence avec les autres threads. Un ensemble d'instructions qui s'exécute en tant qu'unité est dit atomique. En présence d'une accolade fermante (}), l'expression est libérée et l'exécution des threads peut se poursuivre normalement.
Pour ajouter l'instruction lock à votre application
Ouvrez Calculator.cs dans l'éditeur de code.
Localisez chaque instance du code suivant :
varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations;
Il doit y avoir quatre instances de ce code, une dans chaque méthode de calcul.
Modifiez le code comme suit :
lock(this) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; }
Enregistrez votre travail et testez-le comme dans l'exemple précédent.
Vous pouvez remarquer un léger impact sur les performances du programme. Cela est dû au fait que l'exécution des threads s'interrompt lorsqu'un verrou exclusif est obtenu sur votre composant. Bien que cela permette de garantir la précision, cette approche réduit les avantages des threads multiples en termes de performances. Vous devez étudier avec soin la nécessité ou non de verrouiller les threads et n'implémenter cette méthode que lorsqu'elle s'avère absolument nécessaire.
Voir aussi
Tâches
Comment : coordonner plusieurs threads d'exécution
Procédure pas à pas : création d'un composant simple multithread avec Visual Basic
Concepts
Vue d'ensemble du modèle asynchrone basé sur des événements
Référence
Autres ressources
Programmation à l'aide de composants
Procédures pas à pas relatives à la programmation de composants