Tételezzük fel, hogy egy olyan alkalmazást akarunk Ãrni, amely letölti számunkra a legújabb Ubuntu Linux telepÃtÅ‘ ISO fájlját. Ez a művelet a hálózati kapcsolat sebességétÅ‘l függÅ‘en nagyságrendileg lehet pár perc és pár óra is.
Éppen ezért logikus lépés lenne, hogy ne egy szálon fusson az alkalmazásunk. Azonban ez csak a probléma egyik része. A valódi probléma az, hogy ezen szituációban a két szálnak kommunikálnia kell egymással. Például szeretnénk az ESC gomb segÃtségével megszakÃtani a folyamatot és azt szeretnénk, hogy százalékban jelzett visszajelzést kapjunk arról, hogy hogy áll a folyamat.
Ezen követelmények megvalósÃtásához a kommunikációt megoldhatjuk eseményekkel vagy callback függvényekkel.
A callback függvény (vagy visszahÃvási függvény) egy olyan függvény, amit paraméterként adunk át egy másik függvénynek, és az a másik függvény hÃvja meg valamikor a futása során — általában akkor, amikor valamilyen esemény bekövetkezik, vagy egy művelet befejezÅ‘dik.
Nézzünk egy példát, ami a fentebb emlÃtett szituációt szimulálja:
using System;
using System.Threading;
public class Downloader
{
private readonly Action<int> _report;
private readonly CancellationToken _cancellationToken;
private Thread _downloadThread;
public Downloader(Action<int> report, CancellationToken cancellationToken)
{
_report = report;
_cancellationToken = cancellationToken;
_downloadThread = new Thread(Download);
}
private void Download()
{
for (int i = 0; i <= 100; i++)
{
if (_cancellationToken.IsCancellationRequested)
{
_report.Invoke(-1);
//Művelet megszakÃtása
break;
}
//Hosszú művelet szimulálása
Thread.Sleep(30);
//Visszajelzés a folyamat állapotáról
_report.Invoke(i);
}
}
public void Start() => _downloadThread.Start();
}
class Program
{
static void Main()
{
using (var source = new CancellationTokenSource())
{
Downloader downloader = new Downloader(ReportCallback, source.Token);
downloader.Start();
Console.WriteLine("Letöltés elindÃtva. ESC gombbal megszakÃtás");
while (Console.ReadKey(true).Key != ConsoleKey.Escape) ;
source.Cancel();
}
}
private static void ReportCallback(int obj)
{
if (obj == -1)
{
Console.WriteLine();
Console.WriteLine("Letöltés megszakÃtva.");
Environment.Exit(1);
}
else if (obj == 100)
{
Console.WriteLine("Letöltés befejezve.");
Environment.Exit(0);
}
else
{
Console.Write($"{obj}% ");
}
}
}
A program egy lehetséges kimenete:
Letöltés elindítva. ESC gombbal megszakítás
0% 1% 2% 3% 4% 5% 6% 7% 8% 9% 10% 11% 12% 13% 14% 15% 16% 17% 18% 19% 20% 21% 22% 23% 24% 25% 26% 27% 28% 29% 30% 31% 32% 33% 34% 35% 36% 37% 38% 39% 40% 41% 42% 43% 44% 45% 46% 47% 48% 49% 50% 51% 52% 53% 54% 55% 56% 57% 58% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99% Letöltés befejezve.
A programunkban a letöltés szimulációját a Downloader osztály valósÃtja meg. Ennek a konstruktora két paramétert vár: Egy Action<int> tÃpusú delegate-et és egy CancellationToken tÃpust.
A paraméternek adott Action<int> metódus fog meghÃvódni, ha a szálunk állapota változott, a CancellationToken pedig a megszakÃtásért felelÅ‘s. A CancellationToken a .NET egyik beépÃtett leállÃtási mechanizmusa, amit arra terveztek, hogy több szálon, biztonságosan és egységes módon lehessen megszakÃtani aszinkron vagy hosszú futású műveleteket. Igazából laikusan azt gondolhatnánk, hogy egy szimpla bool is elég lenne a megszakÃtás jelzéséhez, azonban ez nem lenne szálbiztos megoldás és könnyen versenyhelyzetet teremthetne.
A tényleges letöltés szimulációjáért a Download() metódus felel. A CancellationToken példányunk IsCancellationRequested tulajdonságával tudjuk ellenÅ‘rizni, hogy érkezett-e megszakÃtási kérelem. Ha igen, akkor a műveletet megszakÃtjuk. A callback függvény meghÃvásával pedig jelzünk a másik szálnak a folyamat állapotáról.
A fÅ‘ metódusban példányosÃtjuk a Downloader osztályunkat. A CancellationToken létrehozásáért és kezeléséért a CancellationTokenSource osztály felelÅ‘s, amely implementálja az IDisposable interfészt.
A while ciklus blokkolja a végrehajtást addig, amÃg a felhasználó nem ESC gombot nyom le. A ReadKey-nek átadott true paraméter pedig arra utasÃtja a konzolt, hogy a lenyomott gombot ne Ãrja ki.
Ha a felhasználó ESC gombot nyomott, akkor a CancellationTokenSource Cancel metódusával jelzésre kerül a megszakÃtási kérelem.
Callback problémák és a megoldásuk
A callback függvények és az események szálak közötti kommunikáció jelzésére alkalmasak, de nem tekinthetőek a legjobb megoldásnak, főleg, ha kettőnél több szál közötti kommunikációt kell levezényelnünk. Könnyen eredményeznek nehezen olvasható és karbantartható kódot, mivel a kód struktúrája nem feltétlen tükrözi ilyenkor azt, hogy a szálak hogyan is vannak egymással kapcsolatban és hirtelen egy olyan szituációban találhatjuk magunkat, amit az irodalom úgy nevez, hogy callback hell.
Éppen ezért ennek elkerülésének érdekében a legtöbb modern programozási nyelv elkezdte alkalmazni a Promise/Future monád alapú megközelÃtést. Ezen mondád lehetÅ‘vé teszi azt, hogy callback metódusok nélkül bevárjuk egy szál eredményét.
A monádokról a tervezési minták részben részletesebben lesz szó, a .NET Future monád implementációjáról pedig a következő alfejezetben.