A Taskok a .NET Framework 4.0-ban mutatkoztak be egyfajta gyorsabb és könnyebb alternatívának a szálak mellé. A klasszikus értelemben véve a Task nem egy szál. Ez egyfajta absztrakció, ami azt teszi lehetővé, hogy egy kódrészletet egy külön helyen futtassunk. Ez történhet egy ThreadPoolból vett szálon, vagy akár ténylegesen külön szálakat indítva, de az is lehet, hogy a kódunk egyáltalán nem fog külön szálra kerülni. Ennek oka az, hogy a Task osztály futásának helyét egy .NET-ben implementált ütemező, a TaskScheduler adja meg. Ez, ha nem bíráljuk felül, akkor a ThreadPool-t fogja használni.
A Task használatának egyik előnye a nyers ThreadPool-al szemben, hogy a Task állapota lekérdezhető, illetve visszaadhat eredményt is. De mielőtt belevágunk a dolgok bugyraiba, nézzünk egy példát a Task egy egyszerű használatára:
using System;
using System.Threading.Tasks;
namespace PeldaTask
{
class Program
{
static void Main(string[] args)
{
Task.Run(() => Console.WriteLine("hello task"));
Console.ReadKey();
}
}
}
A program kimenete:
hello task
Mint látható, a Task típus a System.Threading.Tasks névtérben lakik. Taskokat többféleképpen tudunk létrehozni. A Task.Run() metódussal például vagy a Task.Factory.StartNew() metódussal. Mindkettő bőven rendelkezik variánsokkal. De felmerülhet a kérdés, hogy miért van szükség kétféle létrehozási módszerre. A legtöbb esetben a Task.Run() valamelyik változata bőven elég lesz a céljaink eléréséhez. Ezen metódushívás mindig a ThreadPool-ra fogja küldeni végrehajtásra a feladatunkat. A Task.Factory objektuma pedig haladó létrehozási lehetőségeket kínál, de könnyű lábon lőni magunkat, erről majd később lesz szó.
A Task.Run() az alábbi változatokkal rendelkezik:
public static Task.Run(Func<Task?> function, CancellationToken cancellationToken);
public static Task.Run(Action action, CancellationToken cancellationToken);
public static Task.Run(Func<Task?> function);
public static Task.Run(Action action);
public static Task<TResult>.Run<TResult>(Func<.Task<TResult>?> function);
public static Task<TResult>.Run<TResult>(Func<TResult> function);
public static Task<TResult>.Run<TResult>(Func<Task<TResult>?> function, CancellationToken cancellationToken);
public static Task<TResult>.Run<TResult>(Func<TResult> function, CancellationToken cancellationToken);
A Task típus előnye, hogy közvetlenül adhat vissza értéket és a szálakhoz hasonlóan fogadhat is értéket. Nézzünk egy példát a különböző létrehozási opciókra:
using System;
using System.Threading.Tasks;
internal class Program
{
private static void Main(string[] args)
{
Task.Run(() => Console.WriteLine("Paraméter nélkül"));
// a paraméter típusa object, mivel a Task.Factory.StartNew ezt várja
Action<object> parameteres = (s) => Console.WriteLine($"Task paramétere: {s}");
Task.Run(() => parameteres("Task run hívással"));
Task.Factory.StartNew(parameteres, "Task.Factory.StartNew hívással");
//Érték visszaadás példa
Task<int> calculate = Task.Run(() =>
{
int sum = 1;
for (int i=2; i<=100; i++)
{
sum += i;
}
return sum;
});
// A következő sor bevárja a másik szálon létrejövő eredményt és egy változóba menti azt
int eredmeny = calculate.Result;
Console.WriteLine($"1+2+...+100 = {eredmeny}");
}
}
A program egy lehetséges kimenete:
Task paramétere: Task.Factory.StartNew hívással
Task paramétere: Task run hívással
Paraméter nélkül
1+2+...+100 = 5050
A Task.Factory.StartNew használata a fenti kódban működik, de egy grafikus alkalmazás esetén okozhat meglepetést, ha a Factory.StartNew segítségével indítjuk el a Task futását. Mégpedig azért, mert egy grafikus alkalmazás során (WPF, Windows Forms, stb…) csak a fő szálnak van jogosultsága az általa létrehozott vezérlőelemek kezeléséhez. Ezért a Task.Factory.StartNewa TaskScheduler.Current példányát használja, ami ebben az esetben a fő szálra mutat és nem a ThreadPool-ra. Így egy ilyen alkalmazás esetén a Task időosztással kerül beütemezve a fő szál feladatai közé. Éppen ezért, ha a Task.Factory.StartNewmetódusát szeretnénk használni a Task elindítására, mindig azt az overload-ot használjuk, amely explicit módon elvárja az ütemező megadását.
Taskok állapotának menedzselése
Mi történik akkor, ha szeretnénk tudni a Task állapotát? Ha erre nem lenne szükségünk, akkor alapból dolgozhatnánk szálakkal is a Thread Poolból. Vizsgáljuk meg!
using System;
using System.Threading;
using System.Threading.Tasks;
namespace PeldaTask3
{
class Program
{
static void Main(string[] args)
{
var t = new Task(() => Thread.Sleep(TimeSpan.FromSeconds(2)));
t.Start();
do
{
Console.WriteLine(t.Status);
Thread.Sleep(100);
}
while (!t.IsCompleted);
Console.WriteLine(t.Status);
}
}
}
A program egy kehetséges kimenete:
WaitingToRun
Running
Running
RanToCompletion
A fenti kód először azt fogja mondani, hogy vár az indulásra, aztán futó állapotra vált, majd végül befejezésig futott státuszt kap. A Task Status tulajdonsága egy TaskStatus felsorolásból ad vissza értéket.
Taskok megszakítása
Megszakításra a korábban már használt CancellationToken típus alkalmazható. Ez a .NET egységes megoldása a feladatok megszakítására és a keretrendszerben található metódusok is ezt alkalmazzák. A CancellationToken egy struktúra, így biztonságosan át lehet adni metódusoknak és szálaknak, így csak a CancellationTokenSource tulajdonos szálnak van lehetősége a Cancel() metódus meghívására, ami a Token számára jelzi, hogy megszakítást kértünk.
A korábban bemutatott IsCancellationRequested tulajdonság figyelése mellett a CancellationToken tartalmaz egy ThrowIfCancellationRequested() metódust is, ami a CancellationTokenSource.Cancel() hívásakor egy OperationCanceledException kivételt generál, ami megszakítja a futást. Ez elsőre nem tűnik egy kulturált megoldásnak. Nézzünk erre egy példát:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void LongRunning(CancellationToken token)
{
while (true)
{
Console.WriteLine("Fut");
Thread.Sleep(500);
//ha megszakítás érkezik, akkor kilépünk.
token.ThrowIfCancellationRequested();
}
}
static void Main(string[] args)
{
using (var tokenSource = new CancellationTokenSource())
{
Console.WriteLine("ThrowIfCancellationRequested Demo. Kilépés: Enter");
try
{
var t = new Task(() => LongRunning(tokenSource.Token));
t.Start();
}
catch (OperationCanceledException)
{
Console.WriteLine("Megszakítva");
}
while (true)
{
var consoleKey = Console.ReadKey();
if (consoleKey.Key == ConsoleKey.Enter)
{
//megszakítjuk a HosszuFeladat futását
tokenSource.Cancel();
//2mp várakozás, hogy lássuk a kimenetet
Thread.Sleep(2000);
//Kilép a fő szál is
break;
}
}
}
}
}
A program kimenete:
ThrowIfCancellationRequested Demo. Kilépés: Enter
Fut
Fut
Fut
Fut
Az eredmény lehet meglepő, hogy nem látjuk a „Megszakítva” kiírást sehol, de figyelembe véve, hogy jelen esetben ez a Task egy külön szálon fut, már kevésbé az, hiszen még mindig igaz a szálakról és a kivételekről korábban tanult igazságunk: Egy másik szálon történő kivétel elkapására nincs lehetőségünk.
Taskok eredményének felhasználása
A Task<T> típus rendelkezik egy Result tulajdonsággal, amivel az eredmény kiolvasható. A Result bevárja a feladatvégrehajtást, blokkolván az adott szálat, amennyiben a Task még nem készült el.
A nem generikus Task, illetve a generikus Task több kontrolt adó bevárására a Wait() metódus használható. Ez megvárja, hogy a feladat lefusson. Működése rendkívül egyszerű, ezúttal csak a RanToCompletion állapotot kapjuk meg:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace PeldaTask4
{
class Program
{
static void Main(string[] args)
{
var t = new Task(() => Thread.Sleep(TimeSpan.FromSeconds(2)));
t.Start();
t.Wait();
Console.WriteLine(t.Status);
Console.ReadKey();
}
}
}
A program kimenete:
RanToCompletion
A Wait függvénynek van egy paraméteres verziója, ami egy int vagy TimeSpan timeoutot fogad. Működése a vártnak megfelelő, addig vár, amíg megadtuk. Átadhatunk neki még egy CancellationToken-t is, ha annak az értéke bármikor true lesz amíg a Waittel várunk, a feladat befejezi a munkát egy OperationCancelledException kívétel dobásával.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace PeldaTask5
{
class Program
{
static void Main(string[] args)
{
var t = new Task(() => Thread.Sleep(TimeSpan.FromSeconds(2)));
t.Start();
t.Wait(500);
Console.WriteLine(t.Status);
Console.ReadKey();
}
}
}
Ebben az esetben Running a jelenlegi állapot, míg a következőben közli, hogy a folyamatot megszakítottuk:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace PeldaTask6
{
class Program
{
static void Main(string[] args)
{
try
{
var t = new Task(() => Thread.Sleep(TimeSpan.FromSeconds(2)));
t.Start();
t.Wait(500, new CancellationToken(true));
Console.WriteLine(t.Status);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
}
A program egy lehetséges kimenete ( az adott OS régióbeállításától függő nyelven fogja kiírni):
A művelet végrehajtása megszakadt.
A taskoknak van egy WaitAll függvénye is, itt egy Task tömbre kell meghívni és addig vár, amíg mindegyik be nem fejezte a munkát. Itt arra kell figyelni, hogy ha egy Task-ban le nem kezelt kivétel keletkezik, akkor a minden párhuzamosan futó Task megszakad egy kivétellel.
Taskok egymásra fűzése
A Task-ok egymás után láncolhatóak a ContinueWith metódus segítségével. Ez létrehoz egy új Task-ot, ami akkor fog lefutni, amikor az előző lefutott. A Task tartalmaz statikus függvényeket, amik Task-ok tömbjével való munkát segítik. A WhenAll() olyan Continuation Task-ot hoz létre, ami akkor fut le, amikor a paramétereként megadott tömb összes taskja befejeződött, a WhenAny() pedig olyat, ami akkor, amikor bármelyik befejeződött.
using System;
using System.Threading.Tasks;
namespace PeldaTask8
{
class Program
{
static void Main(string[] args)
{
Task.Run(() =>
{
Console.WriteLine("Első task");
//Példa kedvéért.
throw new Exception();
}).ContinueWith((t) =>
{
//t az előzőnek futott task.
Console.WriteLine("Következő task");
//Az előzőleg nem kezelt kivétel miatt Faulted lesz.
Console.WriteLine("Előző task eredménye: {0}", t.Status);
});
Console.ReadKey();
}
}
}
A program kimenete:
Első task
Következő task
Előző task eredménye: Faulted
Az alábbi példa a WhenAll működését mutatja be:
using System;
using System.Threading.Tasks;
namespace PeldaTask9
{
class Program
{
static void Main(string[] args)
{
Task[] konkurens = new Task[]
{
Task.Run(() => Console.WriteLine("Egyik")),
Task.Run(() => Console.WriteLine("Másik")),
Task.Run(() => Console.WriteLine("Harmadik")),
Task.Run(() => Console.WriteLine("Negyedik")),
};
Task.WhenAll(konkurens).ContinueWith(t =>
{
Console.WriteLine("Vége");
});
Console.ReadKey();
}
}
}
A program kimenete valami hasonló lesz:
Egyik
Harmadik
Negyedik
Másik
Vége