TOC

This article has been localized into French by the community.

Divers:

Programmation Multi-thread avec le BackgroundWorker

Par défaut, chaque fois que votre application exécute une portion de code, ce code est exécuté dans le même thread que celui de votre application. Cela signifie que pendant que ce code est exécuté, rien d'autre ne se produit dans votre application, y compris la mise à jour de votre interface utilisateur.

Il est assez surprenant pour ceux qui sont nouveaux dans la programmation Windows, lorsqu'ils commencent à faire quelque chose qui dure plus d'une seconde et qu'ils réalisent que l'application bloque pendant le traitement. Il en résulte une grande quantité de messages sur les forums de personnes frustrées qui essaient de lancer un process en faisant avancer en même temps une barre de progression, tout cela pour se rendre compte que la barre de progression n'est pas mise à jour pendant que le process est en cours.

Afin d'éviter de bloquer l'interface utilisateur pendant l'exécution d'une action longue, il faut utiliser plusieurs threads. Même si le C# rend la chose plus aisée, le contexte de multi thread apporte également son lot de difficultés. L'utilisation du BackgroundWorker vous permet de travailler avec un thread supplémentaire plus simplement et plus rapidement.

Comment fonctionne le BackgroundWorker

Dans un contexte de multi thread pour une application Windows, le concept le plus difficile concerne le fait que vous ne pouvez pas mettre à jour l'interface utilisateur à partir d'un thread autre que le thread principal de l'application. Si tel est le cas, l'application s'arrête immédiatement. A la place, vous devez appeler une méthode sur le thread principal pour effectuer vos changements. Même si cela semble être un peu lourd, l'utilisation du BackgroundWorker vous simplifie les choses.

Lors de l'exécution d'une tâche sur un thread différent, il y a deux raisons pour lesquelles vous souhaiteriez communiquer avec le reste de l'application : Lorsque vous souhaitez mettre à jour l'avancement de l'exécution de la tâche et bien sûr lorsque vous souhaitez afficher le résultat. C'est autour de ces deux idées que le BackgroundWorker est construit. Ainsi, on retrouve ces deux évènements : ProgressChanged et RunWorkerCompleted.

Il existe un troisième évènement DoWork dans lequel la règle générale est de ne pas mettre à jour l'UI. A la place, vous devez utilisez la méthode ReportProgress qui permet de lancer l'évènement ProgressChanged à partir duquel vous pouvez mettre à jour l'interface utilisateur. Une fois que la tâche est finie, vous devez le signaler au Worker en lui spécifiant un résultat. De cette manière, l'évènement RunWorkerCompleted est levé.

Ainsi, pour résumer, l'évènement DoWork vous permet d'éxécuter du code dans un autre thread et c'est pour cette raison que vous ne pouvez pas mettre à jour l'interface utilisateur. Au lieu de cela, vous importer les données (de l'UI ou d'ailleurs) en utilisant la méthode RunWorkerAsync et les données sortantes sont affectées à la propriété Result du BackgroundWorker.

Cependant, Les évènements ProgressChanged et RunWorkerCompleted sont exécutés dans le thread dans lequel a été créé le BackgroundWorker. Généralement il s'agit du thread principal et par conséquent il est possible de mettre à jour l'interface dans ces deux méthodes. Ainsi, la seule communication qui peut être effectuée entre le code s'exécutant dans le thread secondaire et l'interface utilisateur est la méthode ReportProgress.

Tout ceci est bien sûr de la théorie et bien que le BackgroundWorker soit facile à utiliser, il est aussi important de savoir comment il fonctionne afin d'éviter tout accident car, comme déjà énoncé, les erreurs liées à un contexte de multi thread peuvent vous rendre fou.

Un exemple du BackgroundWorker

Assez avec la théorie - voyons de quoi il s’agit. Dans ce premier exemple, nous voulons un travail assez simple mais long. Chaque nombre entre 0 et 10.000 est testé pour voir s’il est divisible avec le nombre 7. C’est en fait un jeu d'enfant pour les ordinateurs rapides d’aujourd’hui, donc pour prendre plus de temps et prouver notre point, j’ai ajouté un délai d’une milliseconde dans chacune des itérations.

Notre exemple d'application a deux boutons : un qui exécutera la tâche de manière synchrone (sur le même thread) et un qui exécutera la tâche avec un Backgroundworker et donc sur un thread différent. Cela permettra de voir très facilement la nécessité d’un thread supplémentaire lorsque vous faites des tâches chronophages. Le code ressemble à ceci :

<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BackgroundWorkerSample" Height="300" Width="375">
    <DockPanel Margin="10">
        <DockPanel DockPanel.Dock="Top">
            <Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
            <Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
        </DockPanel>
        <ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />

        <ListBox Name="lbResults" Margin="0,10" />

    </DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;

namespace WpfTutorialSamples.Misc
{
	public partial class BackgroundWorkerSample : Window
	{
		public BackgroundWorkerSample()
		{
			InitializeComponent();
		}

		private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			int max = 10000;
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			int result = 0;
			for(int i = 0; i < max; i++)
			{
				if(i % 42 == 0)
				{
					lbResults.Items.Add(i);
					result++;
				}
				System.Threading.Thread.Sleep(1);
				pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
			}
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
		}

		private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
		{
			pbCalculationProgress.Value = 0;
			lbResults.Items.Clear();

			BackgroundWorker worker = new BackgroundWorker();
			worker.WorkerReportsProgress = true;
			worker.DoWork += worker_DoWork;
			worker.ProgressChanged += worker_ProgressChanged;
			worker.RunWorkerCompleted += worker_RunWorkerCompleted;
			worker.RunWorkerAsync(10000);
		}

		void worker_DoWork(object sender, DoWorkEventArgs e)
		{
			int max = (int)e.Argument;
			int result = 0;
			for(int i = 0; i < max; i++)
			{
				int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
				if(i % 42 == 0)
				{
					result++;
					(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
				}
				else
					(sender as BackgroundWorker).ReportProgress(progressPercentage);
				System.Threading.Thread.Sleep(1);

			}
			e.Result = result;
		}

		void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			pbCalculationProgress.Value = e.ProgressPercentage;
			if(e.UserState != null)
				lbResults.Items.Add(e.UserState);
		}

		void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
		}

	}
}

La partie XAML est constituée d'un couple de boutons, un pour exécuter le processus de manière synchrone (sur le thread de la fenêtre) et un pour l'exécuter de façon asynchrone (sur un thread d'arrière plan), d'un contrôle ListBox pour montrer tous les nombres calculés et bien sûr un contrôle ProgressBar en bas de la fenêtre pour voir... la progression, biens-sûr !

Dans la partie "Code-behind", nous commençons avec le handler d’événement synchrone. Comme mentionné , cela boucle de 0 à 10 000 avec un petit délai dans chaque itération, et si le nombre est divisible par 7, alors nous l'ajoutons à la liste. Dans chaque itération nous mettons aussi la barre de progression à jour et une fois terminé, nous affichons un message à l'utilisateur indiquant la quantité de nombres trouvés.

Si vous exécutez l'application et appuyez sur le premier bouton, cela doit ressembler à ceci, quel que soit l'endroit où en est le programme :

Aucun élément dans la liste, pas de progression dans la ProgressBar et le bouton n'apparaît pas relâché, ce qui prouve que nous n'avons aucune mise à jour de l'interface graphique depuis que le clic de la souris a été effectué sur le bouton.

En cliquant plutôt sur le deuxième bouton, on utilisera l’approche BackgroundWorker. Comme vous pouvez le voir dans le code, nous faisons à peu près la même chose, mais d’une façon légèrement différente. Tout le travail est maintenant placé dans l’événement DoWork, que le "Worker" appelle après exécution de la méthode RunWorkerAsync(). Cette méthode récupère les données de votre application qui peuvent être utilisées par le "Worker", comme nous le verrons plus tard.

Comme déjà dit, on ne peut pas rafraîchir l'IHM à partir de l'événement DoWork. A la place, on appelle la méthode ReportProgress sur le "Worker". Si le nombre en cours est divisible par 7, nous l'incluons pour l'ajouter à la liste, sinon nous ne renvoyons que le pourcentage d'achèvement, afin que la ProgressBar puisse être mise à jour.

Une fois tous les nombres testés, on assigne le résultat à la propriété e.Result. Elle sera acheminée vers l'événement RunWorkerCompleted, où nous l'afficherons à l'utilisateur. Ceci peut paraître un peu lourd, au lieu de juste l'afficher à l'utilisateur aussitôt le travail fait, mais encore une fois, cela nous garantit que nous ne communiquons pas avec l'IHM à partir de l'événement DoWork, ce qui est interdit.

Le résultat, comme vous pouvez le voir, est bien plus parlant :

La fenêtre ne se bloque plus, le bouton est cliqué mais pas enfoncé, la liste des nombres possibles est mise à jour à la volée et la barre de progression augmente régulièrement - l'interface est devenue beaucoup plus réactive.

Entrée et Sortie

Notez qu'aussi bien l'entrée, sous forme d'argument passé à la méthode RunWorkerAsync(), que la sortie, sous forme de valeur assignée à la propriété e.Result de l'événement DoWork, sont du type de l'objet. Cela veut dire que vous pouvez leur assigner n'importe quelle valeur. Notre exemple était basique, puisque l'entrée et la sortie peuvent être contenues dans un même entier, mais il est habituel d'avoir des entrées/sorties plus complexes.

Ceci peut être fait en utilisant un type plus complexe, dans bien des cas une structure ou même une classe que vous créez vous-même et que vous faites suivre. En faisant cela, les possibilités sont infinies et vous pouvez transmettre autant de données complexes que vous voulez entre votre BackGroundWorker et l'IHM de votre application.

C'est également vrai pour la méthode ReportProgress. Son second argument s'appelle userState et est de type "object", signifiant que vous pouvez passer ce que vous voulez à la méthode ProgressChanged.

Résumé

Le BackgroundWorker est un excellent outil quand vous voulez faire du multi-tâche dans votre application, principalement parce que c'est très simple à réaliser. Dans ce chapitre, nous avons vu l'une des choses rendues très aisée par le BackgroundWorker, à savoir les rapports d'avancement, mais la prise en charge de l'annulation d'une tâche en cours est également très pratique. Nous examinerons cela dans le prochain chapitre.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!