2

Časově náročné výpočty na pozadí aplikace v .NET

autor Lukáš Kouřil | publikováno 5. září 2007


Každý, kdo se alespoň trochu věnuje programování, se již setkal se situací, kdy některá část programu trvala déle, než je únosné. V takovém případě došlo téměř k „zablokování“ celé aplikace, s kterou nebylo možné,  až do skončení časově náročného algoritmu, doslova hnout, natož pak s ní normálně pracovat. Tyto problémy se velmi často vyskytují ve vyvíjených aplikacích nejen u náročných výpočtů, ale i např. při práci s databází apod. Jak se dá tento problém řešit v .NET Frameworku ukazuje tento článek.

Abychom vše zároveň i vyzkoušeli, vytvoříme velmi jednoduchou aplikaci, na které si použité metody otestujeme.

Začneme tím, že si ve Visual Studiu vytvoříme nový projekt v jazyce C#.

Přidáme do něj odkaz na prostor názvů System.Threading

using System.Threading; 

 a vytvoříme funkci, která nám bude simulovat časově náročný výpočet: 

private void Vypocet() 
{
    for (int i = 0; i < 10; i++)
    {
        Thread.Sleep(1000);
    }
}  

"Výpočet" bude trvat celkem 10s.

Teď si přidáme na plochu aplikace tlačítko, na které dvakrát poklepeme a necháme si vygenerovat metodu pro zpracování událost Click. V této metodě si necháme zavolat funkci výpočet. Celý kód by měl nyní vypadat takto: 

using System; 
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Vypocet()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Vypocet();
            MessageBox.Show("Hotovo!");
        }
    }
}

 Když spustíte tuto aplikaci a kliknete na tlačítko, můžete si ověřit, jak vypadá její běh bez využití zpracování na pozadí. O skončení "výpoctu" budete informováni hlášením.

Použití delegátů

Začněme tím, že si před konstruktorem deklarujeme podobu delegáta. Naše časově náročná funkce nevrací žádnou hodnotu a je bez parametru, proto bude deklarace vypadat následovně: 

private delegate void AsynchonniZpracovani();  

Teď smažeme obsah metody zpracovávající událost Click u tlačítka a nahradíme ho: 

AsynchonniZpracovani az = new AsynchonniZpracovani(Vypocet); 
az.BeginInvoke(null, null);

MessageBox.Show("Hotovo!");

Po spuštění aplikace a kliknutí na tlačítko dojde k okamžitému zobrazení hlášení. Znamená to, že byla funkce použitím delegáta zpracována rychleji než v předešlém případě? Nikoliv. Funkce byla spuštěna asynchronně a tedy nic nebránilo v zobrazení hlášení v MessageBoxu, který byl uveden hned po spuštění delegáta.

Jak tedy odchytit opravdové ukončení časově náročné funkce? K tomu použijeme rozhraní IAsyncResult.

Opět smažeme obsah celé metody Click a nahradíme jej: 

AsynchonniZpracovani az = new AsynchonniZpracovani(Vypocet); 
IAsyncResult iAR = az.BeginInvoke(new AsyncCallback(KonecZpracovani), null);

Delegát AsyncCallback je volán po ukončení asynchronní operace.

Vytvořme si teď metodu KonecZpracovani, kterou zavola delegát AsyncCallback.

 Nejdříve si přidáme odkaz na namespace: 

using System.Runtime.Remoting.Messaging; 

Metoda vypadá takto: 

private void KonecZpracovani(IAsyncResult iAR) 
{
    MessageBox.Show("Hotovo!");
} 

Když spustíte aplikaci, naše náročná funkce je opravdu volána asynchronně a po dokončení operace se zobrazí hlášení. Můžete také kliknout na tlačítko vícekrát.

Celý kód: 

using System; 
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.Remoting.Messaging;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        private delegate void AsynchonniZpracovani();

        public Form1()
        {
            InitializeComponent();
        }

        private void Vypocet()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            AsynchonniZpracovani az = new AsynchonniZpracovani(Vypocet);
            IAsyncResult iAR = az.BeginInvoke(new AsyncCallback(KonecZpracovani), null);
        }

        private void KonecZpracovani(IAsyncResult iAR)
        {
            MessageBox.Show("Hotovo!");
        }
    }
} 

Sekundární vlákna

Vytváření vláken je opravdu jednoduché. Abychom si to předvedli, uveďme naši aplikaci do stavu, než jsme se zabývali delegáty.

Do metody button1_Click napišeme: 

Thread vlakno = new Thread(new ThreadStart(Vypocet)); 
vlakno.IsBackground = true;
vlakno.Start(); 

A to je opravdu vše. Vytvořili jsme nové vlákno. Jako parametr pro konstruktor je uveden delegát ukazující na časově náročnou funkci. Dále jsme nastavili, že vlákno má být spuštěno na pozadí a odstartovali jsme jej. 

Na rozdíl od delegátů vlákna neposkytují jednoduchý způsob jak informovat o jejich ukončení a tak MessageBox s touto informaci dopíšeme do funkce Vypocet.

Tímto jsme si ukázali jak lze jednoduše vytvořit jedno vlákno pro zpracování časově náročného výpočtu, avšak teorie vícevláknového programování již tak jednoduchá není. Je nutné při ní důsladně dbát např. na synchronizaci vláken.

Celý kód aplikace: 

using System; 
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Vypocet()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
            }
            MessageBox.Show("Hotovo!");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Thread vlakno = new Thread(new ThreadStart(Vypocet));
            vlakno.IsBackground = true;
            vlakno.Start();
        } 
    }
}

Komponenta BackgroundWorker

Jako poslední si představíme komponentu, kterou si můžete jednoduše "přetáhnout" do vaší aplikaci, stejně jako třeba tlačítko, TextBox, ComboBox a další. Najdete ji ve Visual Studiu v Toolboxu ve skupině Components.

Jde o nevizuální komponentu, která je vytvořená speciálně pro potřeby zpracování na pozadí a jako jediná umožňuje jednoduše zobrazovat průběh např. výpočtu pomocí ProgressBaru.

Umístěme tedy na aplikaci BackgroundWorker a ProgressBar.

Teď si v konstruktoru upravíme "chování" komponenty BackgroundWorker. Nastavíme, aby BackgroundWorker informoval o svém stavu a připojíme delegáty událostem DoWork, RunWorkerCompleted a ProgressChanged. 

backgroundWorker1.WorkerReportsProgress = true; 

backgroundWorker1.DoWork += 
new DoWorkEventHandler(Pracuji);
backgroundWorker1.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(Hotovo);
backgroundWorker1.ProgressChanged += 
new ProgressChangedEventHandler(Stav);

Následující metoda obsahují volání časově náročné funkce: 

private void Pracuji(object sender, DoWorkEventArgs e) 
{
    Vypocet();
}

Následující metodu spustí delegát po dokončení práce na pozadí: 

private void Hotovo(object sender, RunWorkerCompletedEventArgs e) 
{
    MessageBox.Show("Hotovo!");
} 

Další metoda zajištuje "pohyb" ProgressBaru 

private void Stav(object sender, ProgressChangedEventArgs e) 
{
    progressBar1.Value++;
}

Tím jsme téměř hotoví. Zbývá nastavit maximální hodnotu ProgressBaru. Uděláme to např. v konstruktoru a v našem případě to bude hodnota 10. 

progressBar1.Maximum = 10; 

Upravíme funkci výpočet tak, aby nám "reportovala" změnu ProgressBaru: 

private void Vypocet() 
{
    for (int i = 0; i < 10; i++)
    {
        Thread.Sleep(1000);
        backgroundWorker1.ReportProgress(1);
    }
}

A konečně, na závěr spustíme BackgroundWorker. Do metody button1_Click napíšeme: 

backgroundWorker1.RunWorkerAsync(); 

 I když pro použití BackgroundWorkeru je třeba napsat o trochu víc kódu, je tato komponenta přesně tím, co jsme v našem případě potřebovali. Potřebovali jsme mechanismus, pro zpracování jednho časově náročného výpočtu na pozadí se zobrazením stavu zpracování.

 Tady bych rád upozornil na situaci, kdy se pokusíte kliknout na tlačítkou vícekrát. Na rozdíl od minulých případů se aplikace ukončí s chybou. Jde o to, že BackgroundWorker neslouží pro vytváření vláken, ale doslova, pro zpracování funkcí a metod na pozadí a tudíž nepodporuje souběžné zpracování vláken.

Na závě opět celý kód: 

using System; 
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace WindowsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            progressBar1.Maximum = 10;

            backgroundWorker1.WorkerReportsProgress = true;

            backgroundWorker1.DoWork += 
                new DoWorkEventHandler(Pracuji);
            backgroundWorker1.RunWorkerCompleted +=
                new RunWorkerCompletedEventHandler(Hotovo);
            backgroundWorker1.ProgressChanged += 
                new ProgressChangedEventHandler(Stav);
        }

        private void Vypocet()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
                backgroundWorker1.ReportProgress(1);
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            backgroundWorker1.RunWorkerAsync(); 
        }

        private void Pracuji(object sender, DoWorkEventArgs e)
        {
            Vypocet();
        }

        private void Hotovo(object sender, RunWorkerCompletedEventArgs e)
        {
            MessageBox.Show("Hotovo!");
        }

        private void Stav(object sender, ProgressChangedEventArgs e)
        {
            progressBar1.Value++;
        }
    }
}