This article is currently in the process of being translated into Italian (~98% done).
Multi-threading utilizzando BackgroundWorker
Per impostazione predefinita, ogni volta che l'applicazione esegue un pezzo di codice, questo codice viene eseguito nello stesso thread dell'applicazione stessa. Ciò significa che mentre questo codice è in esecuzione, non accade nient'altro all'interno dell'applicazione, incluso l'aggiornamento dell'interfaccia utente.
Le persone che non conoscono la programmazione Windows, quando fanno per la prima volta qualcosa che richiede più di un secondo rimangono sorprese perchè si rendono conto che la loro applicazione in realtà si blocca. Il risultato sono molti post frustrati del forum di persone che stanno cercando di eseguire un lungo processo durante l'aggiornamento di una barra di avanzamento, e si rendono conto che la barra di avanzamento non viene aggiornata fino a quando il processo non viene eseguito.
La soluzione a tutto questo è l'uso di più thread; C# rende il multi-threading abbastanza facile da fare ma presenta insidie e, per molte persone, non è così comprensibile. È qui che entra in gioco il BackgroundWorker , che rende permette lavorare facilmente con un thread aggiuntivo nella tua applicazione.
Come funziona il BackgroundWorker
Il concetto più difficile sul multi-threading in un'applicazione Windows è il fatto che non è consentito apportare modifiche all'interfaccia utente da un altro thread; in tal caso, l'applicazione si arresterà immediatamente in modo anomalo. Invece, e' necessario invocare un metodo sul thread dell'interfaccia utente (principale) che quindi apporta le modifiche desiderate. Tutto ciò è un po complicato, ma non quando si utilizza BackgroundWorker.
Quando si esegue un'attività su un thread diverso, in genere è necessario comunicare con il resto dell'applicazione in due situazioni: 1. Quando si desidera comunicare a che punto sei nel processo 2. Quando l'attività è terminata e vuoi mostrare il risultato. BackgroundWorker è costruito intorno a questa idea e quindi viene fornito con i due eventi ProgressChanged e RunWorkerCompleted .
Il terzo evento si chiama DoWork e la regola generale è che non puoi toccare nulla nell'interfaccia utente da questo evento. Invece, chiami il metodo ReportProgress () , che a sua volta genera l'evento ProgressChanged , da cui puoi aggiornare l'interfaccia utente. Una volta finito, si assegna un risultato al worker e quindi viene generato l'evento RunWorkerCompleted .
Quindi, per riassumere, l'evento DoWork si occupa di tutto il lavoro. Tutto il codice in esso contenuto viene eseguito su un thread diverso e per questo motivo non ti è permesso toccare l'interfaccia utente da esso. Invece, si portano i dati (dall'interfaccia utente o altrove) nell'evento utilizzando l'argomento sul metodo RunWorkerAsync () e i dati risultanti da esso fuori assegnandoli alla proprietà e.Result.
Gli eventi ProgressChanged e RunWorkerCompleted , invece, vengono eseguiti nello stesso thread in cui viene creato il BackgroundWorker, che di solito sarà il thread principale / dell'interfaccia utente e pertanto è possibile aggiornare l'interfaccia utente da essi. Quindi, l'unica comunicazione che può essere eseguita tra l'attività in esecuzione e l'interfaccia utente è tramite il metodo ReportProgress ().
Questa parte è stata molto teorica, ma anche se BackgroundWorker è facile da usare, è importante capire come e cosa fa, per evitare di fare accidentalmente qualcosa di sbagliato - come già detto, errori nel multi-threading possono portare a problemi fastidiosi.
Un esempio di BackgroundWorker
Basta con la teoria - vediamo di cosa si tratta. In questo primo esempio, vogliamo eseguire un lavoro piuttosto semplice ma dispendioso in termini di tempo. Ogni numero tra 0 e 10.000 viene testato per vedere se è divisibile con il numero 7. Questo è in realtà un gioco da ragazzi per i computer veloci di oggi, quindi per rendere più lungo il tempo e quindi più facile dimostrare il nostro punto, ho aggiunto un un millisecondo di ritardo in ciascuna iterazione.
La nostra applicazione di esempio ha due pulsanti: uno che eseguirà l'attività in modo sincrono (sullo stesso thread) e uno che eseguirà l'attività con un BackgroundWorker e quindi su un thread diverso. Questo dovrebbe rendere molto facile vedere la necessità di un thread aggiuntivo quando si svolgono attività che richiedono tempo. Il codice è il seguente:
<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 parte XAML è composta da un paio di pulsanti, uno per eseguire il processo in modo sincrono (sul thread dell'interfaccia utente) e uno per eseguirlo in modo asincrono (su un thread in background), un controllo ListBox per mostrare tutti i numeri calcolati e quindi un controllo ProgressBar in il fondo della finestra per mostrare i progressi!
In Code-behind, iniziamo con il gestore di eventi sincrono. Come accennato, passa da 0 a 10.000 con un piccolo ritardo in ogni iterazione e se il numero è divisibile con il numero 7, quindi lo aggiungiamo all'elenco. In ogni iterazione aggiorniamo anche ProgressBar e, una volta terminato, mostriamo un messaggio all'utente su quanti numeri sono stati trovati.
Se esegui l'applicazione e premi il primo pulsante, sarà simile a questo, indipendentemente da quanto avanti sei nel calcolo:
Nessun elemento nell'elenco e nessun progresso su ProgressBar e il pulsante non è nemmeno stato rilasciato, il che dimostra che non c'è stato nessun aggiornamento dell'interfaccia utente da quando il mouse è stato premuto sul pulsante.
Premendo invece il secondo pulsante verrà utilizzato l'approccio BackgroundWorker. Come puoi vedere dal codice, facciamo praticamente lo stesso, ma in un modo un po' diverso. Tutto il lavoro viene ora inserito nell'evento DoWork , che il worker chiama dopo aver eseguito il metodo RunWorkerAsync () . Questo metodo prende input dalla tua applicazione.
Come già accennato, non è possibile aggiornare l'interfaccia utente nell'evento DoWork. Invece, chiamiamo il metodo ReportProgress sul worker. Se il numero corrente è divisibile con 7, lo aggiungiamo all'elenco, altrimenti segnaliamo solo la percentuale di avanzamento corrente, in modo che la ProgressBar potrà essere aggiornata.
Una volta testati tutti i numeri, assegniamo il risultato alla proprietà e.Result. Il risultato verrà quindi portato sull'evento RunWorkerCompleted , dove lo mostriamo all'utente. Questo potrebbe sembrare un po' esagerato, invece di mostrarlo all'utente non appena il lavoro è fatto, ma ancora una volta, assicura che non comunichiamo con l'interfaccia utente dall'evento DoWork , che non è consentito.
Il risultato, come puoi vedere, è molto più user friendly:
La finestra non si blocca più, il pulsante viene cliccato ma non soppresso, l'elenco dei possibili numeri viene aggiornato al volo e ProgressBar si aggiorna costantemente - l'interfaccia è diventata molto più reattiva.
Input e output
Si noti che sia l'input, sotto forma dell'argomento passato al metodo RunWorkerAsync () , sia l'output, sotto forma del valore assegnato alla proprietà e.Result dell'evento DoWork, sono del tipo di oggetto. Ciò significa che è possibile assegnare loro qualsiasi tipo di valore. Il nostro era un esempio base, dove sia l'input che l'output potevano essere contenuti in un singolo valore intero, ma è possibile avere input e output più complessi.
Ciò si ottiene utilizzando un tipo più complesso, in molti casi una struttura o addirittura una classe che si crea e si passa. In questo modo, le possibilità sono praticamente infinite e puoi trasportare tutti i dati complessi che desideri tra BackgroundWorker e la tua applicazione / UI.
Lo stesso vale in realtà per il metodo ReportProgress. Il suo argomento secondario si chiama userState ed è un tipo di oggetto, il che significa che è possibile passare tutto ciò che si desidera al metodo ProgressChanged.
Riassunto
BackgroundWorker è uno strumento eccellente quando si desidera il multi-threading nell'applicazione, principalmente perché è così facile da usare. In questo capitolo abbiamo esaminato una delle cose rese molto facili da BackgroundWorker, che è la segnalazione dei progressi, ma anche il supporto per l'annullamento dell'attività in esecuzione è molto utile. Ne esamineremo il prossimo capitolo.