TOC

This article has been localized into Czech by the community.

Ostatní:

Multi-threading s BackgroundWorker

Ve výchozím nastavení, pokaždé když vaše aplikace spustí nějaký kód, tento kód běží na stejném vlákně jako samotná aplikace. To znamená, že zatímco tento kód běží, nic jiného se ve vaší aplikaci neděje, včetně aktualizace vašeho uživatelského rozhraní.

To je docela překvapení pro lidi, kteří jsou nováčky v programování pro Windows, když poprvé udělají něco, co trvá déle než sekundu, a uvědomí si, že jejich aplikace se vlastně zasekne, zatímco to dělají. Výsledkem je spousta frustrovaných příspěvků na fórech od lidí, kteří se snaží spustit dlouhý proces, zatímco aktualizují indikátor průběhu, jen aby si uvědomili, že indikátor průběhu není aktualizován, dokud proces neskončí.

Řešením všeho tohoto je použití více vláken a i když C# toto celkem usnadňuje, vícevláknové programování přináší MNOHO pastí a pro spoustu lidí nejsou zkrátka snadno pochopitelné. Zde přichází do hry BackgroundWorker - umožňuje jednoduše, snadno a rychle pracovat s dalším vláknem ve vaší aplikaci.

Jak BackgroundWorker pracuje

Nejtěžší koncept o vícevláknovém programování v aplikaci pro Windows je fakt, že nemůžete provádět změny v uživatelském rozhraní z jiného vlákna - pokud to uděláte, aplikace okamžitě spadne. Místo toho musíte zavolat metodu na UI (hlavním) vláknu, která pak provede požadované změny. To vše je trochu obtížné, ale ne když používáte BackgroundWorker.

Při vykonávání úlohy na jiném vlákně obvykle potřebujete komunikovat se zbytkem aplikace ve dvou situacích: Když chcete aktualizovat stav ukazující, jak daleko jste v procesu, a samozřejmě, když je úloha dokončena a chcete zobrazit výsledek. BackgroundWorker je postaven kolem této myšlenky a proto přichází se dvěma událostmi ProgressChanged a RunWorkerCompleted.

Třetí událost se nazývá DoWork a obecné pravidlo je, že z této události se nemůžete dotýkat ničeho v uživatelském rozhraní. Místo toho zavoláte metodu ReportProgress(), která zase vyvolá událost ProgressChanged, odkud můžete aktualizovat UI. Jakmile dokončíte, přiřadíte výsledek pracovníkovi (worker) a poté je vyvolána událost RunWorkerCompleted.

Souhrnem, událost DoWork se stará o veškerou těžkou práci. Všechen kód v ní je vykonáván na jiném vlákně a z tohoto důvodu není dovoleno dotýkat se z něj uživatelského rozhraní. Místo toho přenesete data (z UI nebo odkudkoli jiného) do této události použitím argumentu metody RunWorkerAsync(), a výsledná data z ní získáte přiřazením k vlastnosti e.Result.

Události ProgressChanged a RunWorkerCompleted na druhou stranu jsou vykonávány na stejném vlákně, na kterém byl BackgroundWorker vytvořen, což bude obvykle hlavní/UI vlákno, a proto z nich můžete aktualizovat UI. Proto jediná komunikace, která může být provedena mezi vaší běžící úlohou na pozadí a UI, je prostřednictvím metody ReportProgress().

To bylo hodně teorie, ale i když je BackgroundWorker snadno použitelný, je důležité rozumět, jak a co dělá, abyste náhodou neudělali něco špatně - jak již bylo řečeno, chyby ve vícevláknovém programování mohou vést k nepříjemným problémům.

BackgroundWorker - příklad

Dost bylo teorie - pojďme se podívat, o co jde. V tomto prvním příkladu chceme, aby byla provedena poměrně jednoduchá, ale časově náročná úloha. Každé číslo mezi 0 a 10 000 se testuje, zda je dělitelné číslem 7. To je pro dnešní rychlé počítače vlastně hračka, takže abychom to udělali časově náročnějším a tím lépe dokázali naši pointu, přidal jsem zpoždění jedné milisekundy v každé iteraci.

Naše vzorová aplikace má dvě tlačítka: Jedno, které úlohu provede synchronně (na stejném vlákně) a jedno, které úlohu provede s pomocí BackgroundWorker a tím pádem na jiném vlákně. To by mělo velmi snadno ukázat potřebu extra vlákna při provádění časově náročných úloh. Kód vypadá takto:

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

	}
}

Část XAML obsahuje několik tlačítek, jedno pro synchronní spuštění procesu (na UI vlákně) a jedno pro asynchronní spuštění (na vlákně v pozadí), ovládací prvek ListBox pro zobrazení všech vypočítaných čísel a poté ovládací prvek ProgressBar na spodní části okna, který ukazuje... no, průběh!

V Code-behind začínáme s obsluhou synchronní události. Jak bylo zmíněno, cyklus probíhá od 0 do 10 000 s malým zpožděním v každé iteraci a pokud je číslo dělitelné číslem 7, přidáme jej do seznamu. V každé iteraci také aktualizujeme ProgressBar a jakmile jsme hotovi, zobrazíme uživateli zprávu o tom, kolik čísel bylo nalezeno.

Pokud aplikaci spustíte a stisknete první tlačítko, bude to vypadat takto, bez ohledu na to, jak daleko v procesu jste:

Žádné položky v seznamu, žádný pokrok na ProgressBaru a tlačítko nebylo ani uvolněno, což dokazuje, že od doby, co bylo na tlačítko kliknuto, nedošlo k jediné aktualizaci uživatelského rozhraní.

Stisknutí druhého tlačítka místo toho použije přístup BackgroundWorker. Jak můžete vidět z kódu, děláme v podstatě to samé, ale trochu jiným způsobem. Veškerá náročná práce je nyní umístěna v události DoWork, kterou worker zavolá po spuštění metody RunWorkerAsync(). Tato metoda přebírá vstupy z vaší aplikace, které může pracovník využít, o čemž si povíme později.

Jak již bylo zmíněno, nemůžeme aktualizovat uživatelské rozhraní z události DoWork. Místo toho voláme na workerovi metodu ReportProgress. Pokud je aktuální číslo dělitelné sedmi, zahrneme je do seznamu - v opačném případě pouze hlásíme aktuální procento postupu, aby mohl být aktualizován ProgressBar.

Jakmile budou všechna čísla otestována, přiřadíme výsledek k vlastnosti e.Result. To bude poté přeneseno do události RunWorkerCompleted, kde to uživateli ukážeme. Může to vypadat trochu zdlouhavě, místo aby to uživateli bylo ukázáno hned, jakmile je práce hotova, ale opět to zajistí, že nekomunikujeme s uživatelským rozhraním z události DoWork, což není povoleno.

Výsledek je, jak můžete vidět, k uživateli mnohem přívětivější:

Okno již nezamrzá, tlačítko je kliknutelné, ale neblokované, seznam možných čísel se aktualizuje za běhu a ProgressBar stoupá stále nahoru - rozhraní se stalo mnohem reaktivnějším.

Vstup a výstup

Všimněte si, že jak vstup ve formě argumentu předaného metodě RunWorkerAsync(), tak i výstup ve formě hodnoty přiřazené vlastnosti e.Result události DoWork, jsou typu object. To znamená, že jim můžete přiřadit jakoukoli hodnotu. Náš příklad byl základní, protože jak vstup, tak výstup mohly být obsaženy v jediné hodnotě integer, ale je běžné mít vstup a výstup složitější.

Toho lze dosáhnout použitím složitějšího typu, v mnoha případech struktury nebo dokonce třídy, kterou si sami vytvoříte a předáte dál. Tímto způsobem jsou možnosti prakticky nekonečné a můžete mezi svým BackgroundWorkerem a vaší aplikací/uživatelským rozhraním přenášet jakkoliv rozsáhlá a složitá data, jak chcete.

Totéž platí i pro metodu ReportProgress. Její druhý argument se nazývá userState a je typu object, což znamená, že metodě ProgressChanged můžete předat cokoli chcete.

Shrnutí

BackgroundWorker je vynikající nástroj, pokud chcete ve své aplikaci vícevláknové zpracování. Je velmi snadné ho používat. V této kapitole jsme se podívali na jednu z věcí, kterou BackgroundWorker velmi usnadňuje, a to je hlášení průběhu, ale také podpora pro zrušení běžící úlohy je velmi praktická. Podíváme se na to v další kapitole.


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!