Lekce 2 - Vlákna - Příklady vícevláknových aplikací v C# .NET
V minulé lekci, Vlákna - Úvod do vícevláknových aplikací v C# .NET, jsme se uvedli do programování
vícevláknových aplikací v C# .NET. Vysvětlili jsme si pojmy jako proces,
vlákna a jejich synchronizace. Pracovali jsme s vláknem na třídě
Thread
z .NET.
V dnešním tutoriálu si vyzkoušíme naprogramovat dvě vícevláknové aplikace, ve kterých se naučíme vlákna zakládat, pojmenovat a spouštět.
Tisk jedniček a nul
V naší první vícevláknové aplikaci si ukážeme paralelní běh
dvou vláken na střídavém tisku hodnot 1
a
0
do konzole:
Konzolová aplikace
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ...
Založme si novou konzolovou aplikaci s názvem
TiskJednaANul
.
Metoda TiskJedna()
Do třídy Program
si napišme metodu pro tisk hodnoty
1
v nekonečném cyklu while
:
static void TiskJedna() { while(true) { Console.Write("1 "); } }
Metoda TiskNula()
Podobně si přidejme metodu TiskNula()
, která bude v
nekonečném cyklu tisknout hodnotu 0
:
static void TiskNula() { while(true) { Console.Write("0 "); } }
Metoda Main()
Nakonec obě metody zavolejme v metodě Main()
:
using System; using System.Threading; namespace TiskJednaANul { class Program { static void Main() { TiskJedna(); TiskNula(); } ... } }
Testování
Když aplikaci spustíme, vidíme neustálý tisk čísla 1
:
Konzolová aplikace
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ...
Na tisk čísla 0
se samozřejmě nedostalo, protože jsme
zůstali zacykleni v metodě TiskJedna()
. Pojďme to napravit
přidáním dalšího vlákna.
Úprava metody Main()
V metodě Main()
vytvoříme nové vlákno pro tisk nuly pomocí
třídy Thread
:
using System; using System.Threading; namespace TiskJednaANul { class Program { static void Main() { Thread vlaknoNula = new Thread(TiskNula); vlaknoNula.Start(); TiskJedna(); } ... } }
Nejdříve vytvoříme nové vlákno vlaknoNula
, v jehož
konstruktoru zavoláme naši metodu TiskNula()
. Vlákno
vlaknoNula
poté spustíme. Nakonec zavoláme metodu
TiskJedna()
, která bude v nekonečném cyklu tisknout hodnotu
1
do konzole.
Testování
Po spuštění aplikace nyní v konzoli vidíme střídání tisku čísel
1
a 0
, což dokazuje paralelní běh obou vláken:
Konzolová aplikace
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ...
Pořadí tisku 1
a 0
se bude lišit
při každém spuštění aplikace.
V ukázce vidíme, jak operační systém poskytuje plánovač vláken. Plánovač vláken rozhoduje o tom, které vlákno má právo vykonat svůj kód. Plánovač může přidělovat časové kvantum každému vláknu. Když jedno vlákno dosáhne konce svého časového kvanta, plánovač přepne na jiné vlákno.
Úkoly
Představme si aplikaci, která zpracovává úkoly v aplikaci. Založme si
konzolovou aplikaci s názvem Ukoly
. V naší aplikaci bude:
- První vlákno odpovědné za načtení úkolů ("Načítací vlákno").
- Druhé vlákno bude provádět načtené úkoly ("Zpracovávací vlákno").
Výstup z aplikace bude vypadat takto:
Konzolová aplikace
[Načítací vlákno pro úkoly] Načítání nových úkolů...
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolů ...
[Načítací vlákno pro úkoly] Nové úkoly načteny.
[Načítací vlákno pro úkoly] Přiřazování úkolu: Úkol 1: Implementace nového rozhraní.
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolu 1.
[Načítací vlákno pro úkoly] Přiřazování úkolu: Úkol 2: Testování aplikace.
[Načítací vlákno pro úkoly] Přiřazování úkolu: Úkol 3: Oprava chyb v kódu.
[Načítací vlákno pro úkoly] Všechny úkoly přiřazeny.
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolu 2
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolu 3
[Zpracovávací vlákno pro úkoly] Všechny úkoly zpracovány.
Budeme pracovat pouze ve třídě Program
.
Pole assignedTasks
Nejdříve si do třídy Program
deklarujeme statické pole
assignedTasks
:
static string[] assignedTasks = { "Úkol 1 Implementace nového rozhraní", "Úkol 2: Testování aplikace", "Úkol 3: Oprava chyb v kódu" };
Metoda NacitaniUkolu()
V této metodě implementujeme načítání nových úkolů:
static void NacitaniUkolu() { string threadName = Thread.CurrentThread.Name; Console.WriteLine($"[{threadName}] Načítání nových úkolů..."); Thread.Sleep(2000); Console.WriteLine($"[{threadName}] Nové úkoly načteny."); foreach (var task in assignedTasks) { Console.WriteLine($"[{threadName}] Přiřazování úkolu: {task}"); Thread.Sleep(1000); } Console.WriteLine($"[{threadName}] Všechny úkoly přiřazeny"); }
Nejprve si uložíme název vlákna do proměnné threadName
pomocí vlastnosti Thread.CurrentThread.Name
. Poté vytiskneme
název vlákna do konzole spolu s informacemi o průběhu načítání a
přiřazování úkolů.
Metoda ZpracovaniUkolu()
V této metodě budeme načtené úkoly zpracovávat:
static void ZpracovaniUkolu() { string threadName = Thread.CurrentThread.Name; Console.WriteLine($"[{threadName}] Zpracovávání úkolů:"); for (int i = 1; i <= assignedTasks.Length; i++) { Console.WriteLine($"[{threadName}] Zpracovávání úkolu {i}"); Thread.Sleep(2000); } Console.WriteLine($"[{threadName}] Všechny úkoly zpracovány"); }
Nejprve si uložíme název vlákna do proměnné threadName
pomocí vlastnosti Thread.CurrentThread.Name
. Poté vytiskneme
název vlákna do konzole spolu s informacemi o průběhu zpracování
úkolů.
Metoda Main()
A nakonec v metodě Main()
obě vlákna spustíme:
using System;
using System.Threading;
namespace JmenaVlaken
{
class Program
{
static string[] assignedTasks = { "Úkol 1: Implementace nového rozhraní", "Úkol 2: Testování aplikace", "Úkol 3: Oprava chyb v kódu" };
static void Main()
{
Thread nacitaciVlakno = new Thread(NacitaniUkolu);
nacitaciVlakno.Name = "Načítací vlákno pro úkoly";
nacitaciVlakno.Start();
Thread zpracovavaciVlakno = new Thread(ZpracovaniUkolu);
zpracovavaciVlakno.Name = "Zpracovávací vlákno pro úkoly";
zpracovavaciVlakno.Start();
}
static void NacitaniUkolu()
{
string threadName = Thread.CurrentThread.Name;
Console.WriteLine($"[{threadName}] Načítání nových úkolů...");
Thread.Sleep(2000);
Console.WriteLine($"[{threadName}] Nové úkoly načteny");
foreach (var task in assignedTasks)
{
Console.WriteLine($"[{threadName}] Přiřazování úkolu: {task}");
Thread.Sleep(1000);
}
Console.WriteLine($"[{threadName}] Všechny úkoly přiřazeny");
}
static void ZpracovaniUkolu()
{
string threadName = Thread.CurrentThread.Name;
Console.WriteLine($"[{threadName}] Zpracovávání úkolů");
for (int i = 1; i <= assignedTasks.Length; i++)
{
Console.WriteLine($"[{threadName}] Zpracovávání úkolu {i}");
Thread.Sleep(2000);
}
Console.WriteLine($"[{threadName}] Všechny úkoly zpracovány");
}
}
}
V metodě vytvoříme vlákna nacitaciVlakno
a
zpracovavaciVlakno
. Do vlastnosti Name
vložíme
jejich názvy. A pak už jen metodou Start()
obě vlákna
spustíme.
Testování
Po spuštění aplikace uvidíme výpis práce obou vláken:
Konzolová aplikace
[Načítací vlákno pro úkoly] Načítání nových úkolů...
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolů
[Načítací vlákno pro úkoly] Nové úkoly načteny
[Načítací vlákno pro úkoly] Přiřazování úkolu: Úkol 1: Implementace nového rozhraní
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolu 1
[Načítací vlákno pro úkoly] Přiřazování úkolu: Úkol 2: Testování aplikace
[Načítací vlákno pro úkoly] Přiřazování úkolu: Úkol 3: Oprava chyb v kódu
[Načítací vlákno pro úkoly] Všechny úkoly přiřazeny
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolu 2
[Zpracovávací vlákno pro úkoly] Zpracovávání úkolu 3
[Zpracovávací vlákno pro úkoly] Všechny úkoly zpracovány
V příští lekci, Vlákna - Uspání, blokování a stavy vláken v C# .NET , se naučíme základy synchronizace vláken v C# .NET. Ukážeme si, jak vlákna uspat i blokovat, a seznámíme se se stavy vláken.