TOC

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

Misc.:

Multi-threading with the BackgroundWorker

Po definiciji, svaki puta kada vasa aplikacija izvrsava dio koda, taj kod se izvrsava u istoj programskoj niti u kojoj se izvrsava i aplikacija.To znaci da sve dok se taj dio koda izvrsava, nista drugo ne moze da se izvrsava.

Za osobe koje nemaju iskustva sa programiranjem pod windowsima je iznenadjujuce da se program "zaledi" kada prvi puta programiraju nesto sto traje vise od jedne sekunde. To dovodi do bezbrojnih frustriranih poruka i pitanja u forumima od osoba koje pokusavaju da izvrsavaju neki duzi proces i istovremeno osvjezavaju liniju koja pokazuje progres - kada primjete da je linija osvjezena tek nakon sto je proces zavrsen.

Rjesenje je upotreba visestrukih niti. Iako je upotreba visestrukih niti u C#-u vrlo jednostavna, ipak skriva mnoge zamke koje nisu uvjek razumljive. Iz toga razloga je u igru ubacen BackgroundWorker - on cini rad sa dodatnom niti u vasoj aplikaciji brzim i jednostavnim.

How the BackgroundWorker works

Visesnitni rad u windows aplikaciji najkomplikovanijim cini cinjenica da nije dozvoljena promjena grafickog interfejsa iz neke druge programske niti - taj pokusaj obara aplikaciju (program). Umjesto toga treba aktivirati metodu glavne niti (korisnicki interfejs) koja onda cini zeljene promjene. To je prilicno zahtjevno ukoliko ne koristite BackgroundWorker.

Kada se neki zadatak izvrsava u razlicitoj programskoj niti, komunikacija izmedju zadatka i ostatka programa (aplikacije) je obicno potrebna u dva slucaja: kada je potrebno osvjeziti status i prijaviti napredovanje zadatka i kada je zadatak zavrsen i o tome treba informisati glavni program. BackgroundWorker je napravljen sa ta dva zadatka na umu i upravo zbog toga ima dva eventa: ProgressChanged i RunWorkerCompleted.

Treci event je DoWork i iz tog eventa nema pristupa korisnickom interfejsu. Umjesto toga koristi se ReportProgress() metoda koja poziva ProgressChanged event, koji komunicira sa korisnickim interfejsom. Kada je zadatak zavrsen rezultat se dodjeljuje BackgroundWorker-u i aktivira se RunWorkerCompleted event.

o znaci da DoWork event brine o izvrsenju zadatka u razlicitoj programskoj niti i zbog toga zadatak ne moze komunicirati sa korisnickim interfejsom. Umjesto toga , za predaju podataka iz korisnickog interfejsa ili nekog drugog izvora DoWork eventu koristi se argument RunWorkerAsync() metode, a rezultati zadatka se predaju nazad pomocu e.Result atributa.

ProgressChanged i RunWorkerCompleted eventi, se izvrsavaju u istoj niti u kojoj je kreiran BackgroundWorker, a to je obicno nit u kojoj se izvrsava korisnicki interfejs, pa znaci mogu da komuniciraju sa istim. To znaci da se komunikacija izmedju glavne niti i sporedne niti moze odvijati samo pomocu ReportProgress() metode.

Iako je BackgroundWorker relativno jednostavan za upotrebu vazno je razumijeti kako funkcionise da ne bi doslo do gresaka koje u visenitnom izvrsavanju programa mogu dovesti do ozbiljnih problema.

BackgroundWorker - primjer

Prelazimo sa teorije u praksu: zadatak u ovom primjeru je prilicno jednostavan ali uzima dosta vremena. Treba provjeriti koji brojevi izmedju 0 i 10000 (i koliko njih) je djeljivo sa 7. Posto je to za danasnje brze racunare jednostavno svakoj iteraciji je dodata 1ms.

Ova jednostavna aplikacija ima dva dugmeta: Prvo dugme staruje izvrsavanje zadatka sinhronizovano - znaci u istoj programskoj niti u kojoj se izvrsava glavni (korisnicki interfejs) zadatak. Drugo dugme startuje izvrsavenje zadatka asinhrono - u drugoj programskoj niti. Ovaj primjer pokazuje da je druga programska nit potrebna ukoliko se izvrsavaju zadaci koji dugo traju.

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

	}
}

U XAML dijelu su definisana dva dugmeta za startanje sinhronog i asinhronog zadatka, jedno ListBox polje za prikazivanje rezultata i jedan ProgressBar element za prikazivanje napredovanja zadatka.

Izvorni kod pocinje sinhronim modulom za upravljanje dogadjaja (eventa). Taj modul broji od 1 do 10000 sa zastojem od 1 ms pri svakoj iteraciji.Ako je tekuci broj djeljiv sa 7 dodaje se listi. U svakoj iteraciji se takodje osvjezava linija napretka programa.

Ako startujete program i pritisnete prvo dugme lijevo , bez obzira na to koliko je zadatak napredovao, uvijek cete vidjeti istu sliku:

Lista je prazna, na liniji napretka nema promjena, a i dugme je jos uvijek pritisnuto i nije pusteno. To pokazuje da korisnicki interfejs ni jedan jedini put nakon pritiskanja dugmeta nije bio osvjezen.

Ukoliko pritisnete drugo dugme umjesto sinhronog zadatka bice koriscen BackgroundWorker. Zadatak je u principu isti samo se izvrsava na drugaciji nacin. Zadatak se sada izvrsava u DoWork modulu za upravljanje koji se poziva po izvrsenju RunWorkerAsync() metode. Ta metoda preuzima od aplikacije podatke koji se koriste za izvrsenje zadatka, o cemu ce biti govora kasnije.

Kako smo vec napomenuli, korisnicki interfejs ne moze biti osvjezen pomocu DoWork dogadjaja (eventa). Umjesto toga se poziva BackgroundWorker metoda ReportProgress . Ako je broj djeljiv sa sedam upisuje se na listu i prikazuje. Ako broj nije djeljiv sa 7 osvjezava se samo linija napretka.

Nakon sto su svi brojevi provjereni, rezultati se predaju atributu e.Result. To se onda prenosi na RunWorkerCompleted dogadjaj, koji rezultate prikazuje korisniku. ta procedura moze izgledati malo komplikovano u poredjenju sa prikazivanjem rezultata odmah nakon zavrsetka, ali ta procedura je neophodna da bi onemogucila direktnu komunikaciju izmedju DoWork i korisnickog interfejsa.

Rezultat tog pristupa je, kao sto se vidi na slici, mnogo prihvatljiviji za korisnika.

Prozor vise nije "zamrznut", dugme je pritisnuto i pusteno, lista brojeva se blagovremeno osvjezava kao i linija napretka - citav korisnicki interfejs je puno prihvatljiviji.

Input and output

Vazno je primjetiti da su kako ulazni podaci koji su kao argument dati RunWorkerAsync() metodi , tako i izlazni podaci koji su predati atributu e.Result tipa objekt. To znaci da im se moze dodijeliti svaka vrsta podatka. Pokazani primjer je jednostavan posto su i izlazni i ulazni podaci tipa integer, medjutim normalno je koristiti kompleksnije ulazne i izlazne podatke.

U mnogim slucajevima se kao ulazni i izlazni podaci koriste strukture ili kreirane klase, sto znaci da BackgroundWorker pruza bezbrojne mogucnosti razmjene podataka izmedju glavne i sporedne programske niti.

To vazi takodje i za ReportProgress metodu, Njen sekundarni argument je userState tipa objekt, sto znaci da se sve vrste podataka mogu proslijediti ProgressChanged metodi.

Summary

BackgroundWorker je odlican alat za implementaciju visenitnog programiranja upravo zbog jednostavne upotrebe. U ovom poglavlju smo predstavili jednu od stvari koje je jednostavno realizovati pomocu BackgriundWorker-a - javljanje napredovanja zadatka glavnoj niti. BackgriundWorker daje takodje izvrsnu podrsku zaustavljanju zadatka koji se izvrsava u drugij programskoj niti, sto je tema sljedeceg poglavlja.


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!