TOC

This article is currently in the process of being translated into French (~50% done).

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 importez des 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);
		}

	}
}

The XAML part consists of a couple of buttons, one for running the process synchronously (on the UI thread) and one for running it asynchronously (on a background thread), a ListBox control for showing all the calculated numbers and then a ProgressBar control in the bottom of the window to show... well, the progress!

In Code-behind, we start off with the synchronous event handler. As mentioned, it loops from 0 to 10.000 with a small delay in each iteration, and if the number is divisible with the number 7, then we add it to the list. In each iteration we also update the ProgressBar, and once we're all done, we show a message to the user about how many numbers were found.

If you run the application and press the first button, it will look like this, no matter how far you are in the process:

No items on the list and, no progress on the ProgressBar, and the button hasn't even been released, which proves that there hasn't been a single update to the UI ever since the mouse was pressed down over the button.

Pressing the second button instead will use the BackgroundWorker approach. As you can see from the code, we do pretty much the same, but in a slightly different way. All the hard work is now placed in the DoWork event, which the worker calls after you run the RunWorkerAsync() method. This method takes input from your application which can be used by the worker, as we'll talk about later.

As already mentioned, we're not allowed to update the UI from the DoWork event. Instead, we call the ReportProgress method on the worker. If the current number is divisible with 7, we include it to be added on the list - otherwise we only report the current progress percentage, so that the ProgressBar may be updated.

Once all the numbers have been tested, we assign the result to the e.Result property. This will then be carried to the RunWorkerCompleted event, where we show it to the user. This might seem a bit cumbersome, instead of just showing it to the user as soon as the work is done, but once again, it ensures that we don't communicate with the UI from the DoWork event, which is not allowed.

The result is, as you can see, much more user friendly:

The window no longer hangs, the button is clicked but not suppressed, the list of possible numbers is updated on the fly and the ProgressBar is going steadily up - the interface just got a whole lot more responsive.

Input and output

Notice that both the input, in form of the argument passed to the RunWorkerAsync() method,as well as the output, in form of the value assigned to the e.Result property of the DoWork event, are of the object type. This means that you can assign any kind of value to them. Our example was basic, since both input and output could be contained in a single integer value, but it's normal to have more complex input and output.

This is accomplished by using a more complex type, in many cases a struct or even a class which you create yourself and pass along. By doing this, the possibilities are pretty much endless and you can transport as much and complex data as you want between your BackgroundWorker and your application/UI.

The same is actually true for the ReportProgress method. Its secondary argument is called userState and is an object type, meaning that you can pass whatever you want to the ProgressChanged method.

Summary

The BackgroundWorker is an excellent tool when you want multi-threading in your application, mainly because it's so easy to use. In this chapter we looked at one of the things made very easy by the BackgroundWorker, which is progress reporting, but support for cancelling the running task is very handy as well. We'll look into that in the next chapter.

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!