.NET 4 Cancellation Model

A .NET 4 egy új, univerzálisnak szánt modelt vezetett be, folyamatban levő műveletek megszakítására. Ilyen irányú próbálkozások eddig is voltak, például az Event-based Asynchronous Pattern által használt CancelAsync() függvények. A CancelAsync() azonban nem más, mint egy név konvenció. Annyiban segít, hogy a programozó hamarabb kikeresi a dokumentációban, hogy mit kell hívnia, ha meg akar szakítani egy műveletet. Nem segít azonban abban, ha több komponens működik együtt, és ezeken a komponenseken kell valahogy végigvinni a megszakítási igényt.

Miért nem alakítottak ki modelt erre az elmúlt 10 évben, és miért lett fontos ez éppen most? Azért, mert most válik nagyon hangsúlyossá az az irányvonal, hogy egy művelet végrehajtásában több, apró kódrész vesz részt, amelyeknek együtt kell működni a feladat végrehajtásában, vagy éppen a végrehajtás megszakításában. Ha használjuk a Task osztályt, és a megoldandó feladatot szétosztogatjuk további Task példányok között, akkor mi magunk is szeretnénk valami hatékony eszközt ahhoz, hogy ha a szülő Task futását meg kell szakítani, akkor a létrehozott al-taszkok is leállíthatóak legyenek.

Kézi módszerek

Feladatok megszakítására eddig is szükség volt, és ezt a programozók általában maguk implementálták. Legegyszerűbb egy boolean jellegű változót bevezetni, amelyet a programkód időnként ellenőriz. Az ilyen megoldásokkal két alapvető gond van.

Az egyik, hogy a kézi módszerek csak a saját kódjainkban működnek. Honnan tudná egy más által írt programkód, például egy vásárolt programkönyvtár függvénye, hogy mit kell nézni a művelet megszakításához? Ha szerencsénk van, akkor az adott kódnak meg van a saját módszere a műveletek megszakítására, és akkor a saját kis flag-ünket át tudjuk játszani.

A másik probléma, hogy a boolean flag-ek egy mindenki által elérhető villanykapcsolóként működnek, azaz ki-be kapcsolgathatja bárki, aki hozzáfér. Saját kódunkban persze betartjuk az általunk kitalált protokolt – amíg a program nem nő túl nagyra, és nem egy 10 fős csapatban dolgozunk rajta. Kis odafigyeléssel írhatunk okosabb osztályokat a cancellation kezelésére, ekkor már csak a megoldás egyedisége marad hátrányként, azaz továbbra sem működik együtt más kódokkal.

Kis történeti érdekességnek meg kell említeni, hogy a Task Parallel Library Béta 2 előtt szintén csak “spéci” megoldást adott a Cancellation-re, amelyet csak a Task osztály implementált. Ebben a felállásban a Task osztály rendelkezett egy Cancel() függvénnyel. A Béta 2-es verzió már az általánosított modellel jelent meg.

.NET 4 Cancellation

A .NET által bevezetett Cancellation Model egyik célja, hogy a programkódok egységesen oldják meg a műveletek visszavonását. Ennek több előnye van, mint a rövidebb tanulási idő, illetve a programrészek ilyenterű kompatibilitása. A .NET Cancellation modelje nem a bármelyik kódrész által vezérelhető villanykapcsoló elvét követi. Hogy hogyan működik, azt próbálja érzékeltetni a következő ábra:

Az egyik tervezési elv az volt, hogy a cancel indítását, illetve a cancel igényének ellenőrzését kettéválasszák. Így adva van egy eszköz, amin keresztül kezdeményezni lehet a visszavonási igényt. Ezt egy kis rádióadónak lehet elképzelni, egyetlen Cancel gombbal, amit egyszer lehet megnyomni, ezután úgy marad. Ennek az adónak a jelzését veszik kicsi vevők, amin a jelzés kigyullad, ha az adó visszavonási igényt sugároz.

A kicsi vevők kerülnek azokhoz a programkódokhoz, amelyeket esetleg meg kell szakítani. Az a programra van bízva, hogy mikor “néz rá” a vevő jelzésére, illetve hogy egyáltalán foglalkozik-e a jelzéssel. Ezt nevezik kooperatív megszakításnak. Nem kooperatív megszakításra bizonyos korlátokkal eddig is volt lehetőség például a Thread.Abort() hívással.

A példába kevéssé illik bele, de a vevőket tetszőleges számban lehet klónozni, a klónozott vevők ezután ugyanazt az adót hallgatják, amelyet az eredeti vevő.

Az adó szerepét a .NET esetén a CancellationTokenSource osztály valósítja meg, a vevők pedig CancellationToken típusú struktúrák. Egy adót az egyszerű var cts = new CancellationTokenSource() példányosítással hozhatunk létre, az adóra hallgató első vevőt pedig a cts.Token property lekérdezésével. Ezután vagy további cts.Token hívásokkal, vagy egy már meglévő token másolásával hozhatunk létre új vevőket. Mivel a CancellationToken egy értéktípus, különösebb erőforrásigény nélkül sokszorosítható illetve adható tovább paraméterként.

Egy konkrét példa látható a következő kódrészletben:


using System;
using System.Threading;
using System.Threading.Tasks;

namespace Cancellations
{
  class Program
  {
    // Egy Task indítása, amit le fogunk állítani futás közben. 
    // Az érdekes rész a 28. sorban van.
    static Task DoAsyncTasks(CancellationToken cancellationToken)
    {
      // Egy taszk indítása. A taszk csak számol, néha ellenőrzi a cancellation tokent,
      // amit closure változóként használ, nem pedig a StartNew()-nak átadott utolsó 
      // paraméterből (39 sor) veszi. A delegate semmit nem tud arról, hogy egy taszkon 
      // belül fut, így el sem éri a taszknak átadott cancellation token-t.
      return Task.Factory.StartNew(
               () => {
                       long cntr = 0;
                       for (int i = 0; i < 1000000; i++)
                       {
                         for (int n = 0; n < 1000000; n++)
                         {
                           cntr++;                   // csak hogy csináljon valamit
                         } // for n

                         // Bizonyos időközönként a cancellation "vevő" ellenőrzése
                         cancellationToken.ThrowIfCancellationRequested();

                         // A fenti sor ugyanazt csinálja, mint a következő,
                         // tehát exceptiont dob.                                
                         // if (cancellationToken.IsCancellationRequested)
                         // {
                         //   throw new OperationCanceledException(token);
                         // } // if
                       } // for i

                       return cntr;
                     },  
                cancellationToken); // cancellation "vevő", értéktípusként másolódik
    } // DoAsyncTasks()

    static void Main(string[] args)
    {
      // Egy "adó" létrehozása.
      var tokenSource = new CancellationTokenSource();

      // Taszk indítása, paraméterként a cancellation adóhoz tartozó vevő megy át.
      // értéktípus lévén a cts.Token értéke olcsón másolgatható.
      var task = DoAsyncTasks(tokenSource.Token);
            
      // Egy kis ideig számolhasson a taszk, majd a művelet leállítása.
      Thread.Sleep(2000);

      // Ez a hívás olyan állapotba állítja CancellationTokenSource-ot, hogy a 
      // CancellationToken.IsCancellationRequested kérések true értéket adjanak eredményül.
      tokenSource.Cancel();

      // A Taszk fel van készítve az OperationCanceledException-re, és megfelelő állapotba
      // kerül. Erről a következő cikkek egyikében lesz bővebben szó.
      Thread.Sleep(2000);
      Console.WriteLine(task.Status);  // Az állapot riportolása.
                   
      Console.ReadLine();
    } // Main()
  } // classs Program
} // namespace Cancellations

A példakódban a 45. sorban jön létre a CancellationTokenSource, ami az adó szerepét játssza. Vevők sok helyen jönnek létre a kódban, de mindenképpen legalább egy CancellationTokenSource.Token property lekérdezésnek kell lennie, ami az első másolható tokent hozza létre. A példakódban ez a 49. sorban található. A 28. sor miatt egy closure változó jön létre, a taszk által futtatott delegate ezen keresztül értesül arról, ha a CancellationTokenSource (az adó) már olyan állapotba került, ami a megszakítási igényt jelzi. Maga a taszk szintén eltárol egy tokent az 39. sornál, erre a belső mechanizmusának van szüksége. Amikor a 28. sorban egy OperationCanceledException kivételt dobunk, azt a Task példány egy ponton elkapja. Amennyiben látja, hogy a kivétel ugyanahhoz a CancellationTokenSource-hoz tartozó tokent tartalmazza, mint amit korábban eltárolt (39. sor, mint paraméter), a kivételt egy kooperatív megszakítási folyamatnak veszi, és a kivételt nem dobja tovább, hanem Cancelled állapotba áll át.

Hogyan kommunikál a Token és a TokenSource?

Az adó-vevő példa annyiban félrevezető, hogy azt sejteti, a CancellationTokenSource valamilyen módon lekommunikálja a CancellationToken-nek, amikor a Cancel() függvényét meghívják. A valóság azonban ennél sokkal-sokkal egyszerűbb. A CancellationToken, mint értéktípus, csak egy referenciát hordoz arra a CancellationTokenSource-ra, amelyhez tartozik. Az egészet körülbelül így kell elképzelni:


struct CancellationToken
{
  private CancellationTokenSource source;
  
  public bool IsCancellationRequested 
  { 
    get 
    { 
      return this.source.IsCancellationRequested; 
    } // get
  } // IsCancellationRequested 

  ... egyéb műveletek, mind a this.source-nak delegál
} // struct CancellationToken

A kódot elnézve az az érzésünk támadhat, hogy ez egy megcsavart interfész a CancellationTokenSource-on. Igen, meg lehetett volna oldani ezt az egészet úgy, hogy a CancellationTokenSource implementál egy ICancellationToken interfészt, és a mostani CancellationToken struktúra helyett az interfész referenciát kellene adogatni. Miért nem így történt? Ezt csak találgatni lehet, de szerintem a következő van a háttérben:

Az interfész és az azt megvalósító osztály között egy “olyan mint egy” kapcsolat van. Ez alapján a logika alapján, ha a CancellationTokenSource megvalósít egy ICancellationToken-t, akkor az azt fejezi ki, hogy a CancellationTokenSource “olyan mint egy” CancellationToken. Ez élesen szembe megy azzal a törekvéssel, hogy a cancellation indítását elválasszák a cancellation igény detektálásától.

Ha a CancellationTokenSource valósítaná meg az ICancellationToken interfészt, akkor a fejlesztők a CancellationTokenSource-ot kezdenék el adogatni a kódon keresztül. Bár a kódban a fejlesztő ICancellationToken referenciát látna, tudná, hogy emögött egy CancellationTokenSource példány van (hiszen a hívási lánc elején lehet, hogy ő adta oda), és egy gyengébb pillanatában esetleg hajlana rá, hogy ezt visszacastolja az eredeti típusra, mert úgy könnyebben megold valamit. Tegye fel a kezét, aki nem látott még ilyen kódot.

Akárhogyis, az, hogy a CancellationToken mindent a CancellationTokenSource-nak delegál, csak egy implementációs részlet, ez a tudás néhány helyzetben segíthet megérteni a dolgokat, más esetekben továbbra is az adó-vevő modelt követve tudunk könnyebben dolgozni.

Értesítési módok

A fenti szemléltető ábra CancellationTokent egy piros lámpának ábrázolta, amire néha rá kell tekinteni, hogy a visszavonási igényt észrevegyük. Ez a “rátekintés” a CancellationToken.IsCancellationRequested() vagy CancellationToken.ThrowIfCancellationRequested() hívás. Vannak azonban más módok is, ahogyan a visszavonási igényt detektálni lehet.

Előfordul például, hogy egy feladat egy esemény bekövetkeztére várakozik, valamilyen szinkronizációs objektumot felhasználva:


var manualEvent = new ManualResetEvent(false);

Task.Factory.StartNew(
    () =>
    {
        Console.WriteLine("Előfeltétel kialakítása...");
        Thread.Sleep(20000);  // Ez lenne a kemény munka az előfeltételhez
        Console.WriteLine("Előfeltétel teljesült");

        manualEvent.Set();    // Jelezni a feltételre várakozóknak
    });

Task.Factory.StartNew(
    () =>
    {
        Console.WriteLine("Várakozás az előfeltételre...");

        manualEvent.WaitOne();

        Console.WriteLine("Művelet folytatása");
    });

Amíg a második taszk a ManualResetEvent-re várakozik, addig nem tud a CancellationToken-re figyelni. Megoldható lenne, hogy a WaitOne() kap egy paramétert, hogy bizonyos idő lejártával térjen vissza, így ciklusban lehetne ellenőrizni a CancellationToken.IsCancellationRequested értékét. Lehetőség van azonban arra, hogy a CancellationToken-től egy WaitHandle-t kérünk, amit utána át lehet adni szinkronizációs függvényeknek:


var manualEvent = new ManualResetEvent(false);
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

Task.Factory.StartNew(
    () =>
    {
        Console.WriteLine("Előfeltétel kialakítása...");
        Thread.Sleep(20000); 
        Console.WriteLine("Előfeltétel teljesült");

        manualEvent.Set();    // Jelezni a feltételre várakozóknak
    });

Task.Factory.StartNew(
    () =>
    {
        Console.WriteLine("Várakozás az előfeltételre...");

        WaitHandle.WaitAny(
            new[] 
            { 
                manualEvent, 
                token.WaitHandle 
            });

        token.ThrowIfCancellationRequested();

        Console.WriteLine("Művelet folytatása");
    }, token);

Thread.Sleep(2000);   // Egy kicsit dolgozzon az első taszk, és várakozzon a második
tokenSource.Cancel(); // Feladat visszavonása

A második Task most a WaitHandle.WaitAny() függvény segítségével két szinkronizációs objektumra tud várakozni, és bármelyik objektum aktiválódása esetén visszatér. Az 27-ik sor miatt, ha a WaitAny() a CancellationToken miatt tért vissza, vagy ha nem amiatt tért vissza, de eközben a műveletet a CancellationToken-en keresztül amúgy is megszakították, a taszk futása befejeződik.

A .NET 4 a fenti esetre egyébként rendelkezik jobban használható szinkronizációs objektumokkal, a ManualResetEventSlim osztály figyelembe tudja venni a CancellationToken-t:


var manualEvent = new ManualResetEventSlim();
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

Task.Factory.StartNew(
    () =>
    {
        Console.WriteLine("Előfeltétel kialakítása...");
        Thread.Sleep(20000);
        Console.WriteLine("Előfeltétel teljesült");

        manualEvent.Set();
    });

Task.Factory.StartNew(
    () =>
    {
        Console.WriteLine("Várakozás az előfeltételre...");

        manualEvent.Wait(token);   // Várakozik a manualEvent-re és a tokenre is.
                                   // Nem kell a token-t külön vizsgálni, a Wait()
                                   // már eldobta az OperationCanceledException-t, ha kellett
        Console.WriteLine("Művelet folytatása");
    }, token);

Thread.Sleep(2000);
tokenSource.Cancel();

Console.ReadLine();

Ebben az esetben a manualEvent.Wait(token) hívás egy OperationCanceledException-t dob, ha a token aktivizálódik. Pontosan ezt kellene nekünk is tennünk, hogy együttműködjünk a Task osztály infrastruktúrájával, emiatt nincs is szükség egyéb vizsgálatra vagy műveletre. A Task példány Cancelled állapotba kerül, miután az 27. sorban a Cancel() függvényt meghívjuk.

Cancellation Callback regisztrálása

A Token.WaitHandle már egész sor lehetőségnek nyit utat, például a ThreadPool.RegisterWaitForSingleObject() segítségével egy callback függvényt lehetne indítani akkor, amikor a CancellationToken aktiválódik. Ez a callback függvény egy bonyolultabb taszk esetében elvégezhet takarítási munkákat, vagy konzisztens állapotba hozhatja a részeredményeket. Mivel ilyen utómunkákra szükség lehet, a CancellationToken közvetlenül is lehetőséget ad callback megadására a CancellationToken.Register() függvény segítségével. Nézzük a következő példát:


var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

token.Register(
    () => { 
              Console.WriteLine("callback 1"); 
          });

token.Register(
    () => {
              Console.WriteLine("callback 2");
          });

tokenSource.Cancel();

A Cancel() hívás hatására mind a két callback függvény meghívódik egymás után. Belső megvalósításban az történik, hogy mint mindent, a Register() hívást a CancellationToken példány a CancellationTokenSource-nak delegálja. A CancellationTokenSource egy listát tart karban a hívandó callback kódokról, amelyet sorra vesz, és meghívogat a CancellationTokenSource.Cancel() függvény hívásakor. Ennek megfelelően az alapeset az, hogy a callback-ek azon a szálon hajtódnak végre, amelyiken a Cancel() függvény meg lett hívva.

Ha ez nem megfelelő, regisztrációnál megadhatjuk, hogy a regisztráció tárolja el az aktuális szinkronizációs kontextet, amelyről többet olvashatunk az ExecutionContextről szóló cikkben. Egy GUI szálon hívott callback regisztráció esetén így a callback is a GUI szálon fut majd le. Ehhez csak egy plusz paramétert kell megadni:


token.Register(
    () => { 
              Console.WriteLine("callback 1"); 
          },
    true);

Hogy ennek a gyakorlatban mennyi haszna van, nehéz megítélni. GUI programok esetén a Cancel() hívása valószínűleg egy Cancel gomb eseménykezelőjéből indul, azaz eleve a GUI szálon futnak le a callback függvények. A plusz paraméternek akkor van értelme, ha a regisztráló kód a GUI szálon fut, a Cancel() hívása viszont nem a GUI szálon fut majd le.

A SynchronizationContext-en kívül a Register függvény az ExecutionContext-et is eltárolja. Ez már hasznosabb lehetőségeket hordoz magában, ami a következő példán látszik:


var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

Task.Factory.StartNew(
  () =>
  {
    CallContext.LogicalSetData("Data", "Almafa"); // Kontextus változó a logikai szálon

    Console.WriteLine(                            // Thread id kiírása, a thread pool
      "Regisztrálás Thread Id: {0}",              // egyik szála lesz, mivel ez a kód a 
      Thread.CurrentThread.ManagedThreadId);      // Thread Pool-on fut.

    token.Register(                               // Cancellation Callback regisztrálása
      () => { 
              Console.WriteLine(                  // A callback kiírja a kontextus változót,
                String.Format(                    // pedig a főszálon fut le. Ez az 
                  "Logical data: {0}, Thread Id: {1}", // ExecutionContext-nek köszönhető
                  CallContext.LogicalGetData("Data"),
                  Thread.CurrentThread.ManagedThreadId)); 
            });

    Thread.Sleep(20000);
  });


Thread.Sleep(2000);
tokenSource.Cancel();

Console.WriteLine("Main Thread Id: " + Thread.CurrentThread.ManagedThreadId);

A program egy olyan taszkot indít, amely kontextusváltozónak megad egy értéket (“Almafa”). Ezután kiírja az aktuális szál id-ját, ami a Thread Pool egyik száljának id-ja lesz. Ezután regisztrál egy kódot, amit meg kell hívni Cancel() esetén. Ez a callback kiírja a kontextus értéket, illetve az aktuális szál id-ját. Amikor az 26. sorban a Cancel() függvény meghívódik a fő szálról, a callback függvények a fő szálon futnak le. Az általunk megadott kód így egy másik id-t ír ki, mint amin a taszk futott, de a kontextus változót mégis eléri:

Ideiglenes regisztráció

Ha a Cancellation Callback függvények szerepe a félbemaradt munka konzisztens állapotba juttatása, előfordulhat, hogy erre a szerepre csak ideiglenesen van szükség. Ha a munka eljutott egy pontig, nem biztos, hogy onnantól a callback függvényre már szükség van. Emiatt a callback regisztrálását vissza lehet vonni.

A visszavonás mechanizmusa kicsit furcsa, de jól használható. Miért furcsa? Kiderül a következőkből:

A CancellationToken.Register() függvény egy CancellationTokenRegistration struktúrát ad vissza. Ennek a struktúrának nincs túl sok használható függvénye. Ami különleges benne, hogy értéktípus volta ellenére megvalósítja az IDisposable interfészt. Miért furcsa, ha egy értéktípus IDisposable? Nézzük a következő példát:


struct Alma : IDisposable
{
  public void Dispose() {..}
}


Alma a = ...;
Alma b = a;

a.Dispose();
// b.Dispose kell?

Miért vagyunk zavarban b esetében? Ha Alma referencia típus lenne, a és b ugyanarra a példányra mutatna, és a helyzet tiszta lenne, a.Dispose() elrendezi a dolgokat, b-n keresztül már nem kell meghívni. Most azonban értéktípusokról van szó. A kód elején “b” átmásolta “a” teljes állapotát (bár értéktípusok esetén nehéz állapotban gondolkodni). Azt azonban nem tudjuk, hogy ebbe a “teljes állapotba” bele tartozik-e az az erőforrás, ami miatt a Dispose-t hívogatni kell. Emiatt értéktípusokra nem szoktak IDisposable interfészt rakni, bár saját véleményem szerint ha egy konstrukció olyan bonyolult, hogy IDisposable szükséges, akkor az már koncepcionálisan nem felel meg értéktípusnak.

A CancellationTokenRegistration esetében szerencsére nem kell fájjon a fejünk a Dispose() hívása miatt. Semmi olyan nincs a háttérben, ami miatt azt kötelező, vagy erősen ajánlott lenne meghívni. Az egyetlen ok, ami miatt be lett vezetve, hogy egy Deregister() függvénynél sokkal kényelmesebb használatot tesz lehetővé. Hogyan? Nézzük a következő kódot:


// Ez a Cancellation Callback függvény:
static void Recover()
{
    Console.WriteLine("recovering");
} // Recover()

// Ez végzi a számítást, illetve ellenőrzi a visszavonási igény. Most a 
// számítás rész kimarad, mert nem lényeges
static int Calc(CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    return 0;
} // Calc()

static void Main(string[] args)
{
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;

    Task.Factory.StartNew(
        () =>
        {       
            using (token.Register(new Action(Recover)))
            {
                // A számítás ezen része igényel csak helyreállítást visszavonás esetén.
                // mivel valós számítás nincs a kódban, 5 másodpercre mérünk
                var startTime = DateTime.Now;
                while (DateTime.Now.Subtract(startTime).TotalSeconds < 5 )
                {
                    Calc(token);
                } // while
            } // using

            // Innentől nincs Cancellation Callback
            Console.WriteLine("Deregistered");

            while (true)
            {
                token.ThrowIfCancellationRequested();
            } // while

        });


    Thread.Sleep(2000); // Thread.Sleep(7000);
    tokenSource.Cancel();

    Console.ReadLine();
} // Main()

A CancellationTokenRegistration használata a 23. sorban látható. Az IDisposable miatt a “using” használatával kényelmesen kijelölhető az a kódrészlet, ahol szeretnénk, ha a Cancellation Callback függvényünk meghívódna. Ez a kódrész 5 másodpercig fut. Ha a 44. sorban 5 másodpercen belül triggereljük a CancellationTokenSource példányt, akkor a Recover() függvény le fog futni. Ha 5 másodpercnél többet várunk, az 23-32 közötti kódrészt elhagyva a regisztráció érvényét veszti, ezután a Recover() függvény már nem fut le.

Összekapcsolt tokenek

Az eddig bemutatott model egyszerűnek és mégis jól használhatónak tűnik. Egy kis csúnyaság, ha több forrásból kell figyelni a visszavonási igényeket, akkor több tokent kell adogatni-ellenőrizgetni. Ez nem olyan valószínűtlen helyzet, mint ahogy elsőre látszik. Tegyük fel, hogy van egy “kockázatos” művelet, amit nem biztos, hogy sikerül befejezni. Lehet kockázatos például azért, mert Zimbabwe-ből kell folyamatosan adatokat letölteni a számításokhoz. Ha az adatfolyam megszakad, akkor a már elindított párhuzamos számításokat esetleg nincs értelme befejezni. Ugyanakkor a számításokat a felhasználó is megszakíthatja a felhasználói felületről. Így van két cancellation forrás, mind a kettőhöz tartozó tokenre figyelni kell.

Hogy ne kelljen kettő vagy több tokenre figyelni, a következő ábrán látható felállás lenne számunkra hasznos:

A kép baloldalán van két Cancellation forrás, és egy-egy ezekre hallgató token.Eredeti felállásban mind a két tokenre figyelni kellene. A kép jobboldali felén azonban egy újabb Cancellation forrás van, méghozzá úgy megberhelve, hogy az előbbi két token rá van kötve. Amikor valamelyik token aktiválódik, az triggereli a jobboldali Cancellation forrást, így elég az ehhez a forráshoz tartozó tokent ellenőrizni.

A fenti felállást az eddigi lehetőségek ismeretében könnyen megvalósíthatjuk kódból:


var tokenSource1 = new CancellationTokenSource();
var token1 = tokenSource1.Token;

var tokenSource2 = new CancellationTokenSource();
var token2 = tokenSource2.Token;

var linkedTokenSource = new CancellationTokenSource();

token1.Register(() => linkedTokenSource.Cancel());
token2.Register(() => linkedTokenSource.Cancel());

var linkedToken = linkedTokenSource.Token;

tokenSource1.Cancel();
Console.WriteLine(linkedToken.IsCancellationRequested);

A példakód a szemléltető ábra összekötő vezetékeit Cancellation Callback kóddal oldja meg (9-10 sorok). Amikor a két eredeti Cancellation forrás valamelyikét aktiválják, az ahhoz tartozó callback függvény aktiválja a linkedTokenSource-ot is. Abból nincs probléma, ha később a másik token source is aktív lesz, mivel egy ismételt Cancel() hívás a CancellationTokenSource példányon nem csinál semmit.

A fenti kód helyett használható a következő, ami egyébként a hivatalos módja az eredeti probléma megoldásának:


var tokenSource1 = new CancellationTokenSource();
var token1 = tokenSource1.Token;

var tokenSource2 = new CancellationTokenSource();
var token2 = tokenSource2.Token;

var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
var linkedToken = linkedTokenSource.Token;

tokenSource1.Cancel();
Console.WriteLine(linkedToken.IsCancellationRequested); 

A CreateLinkedTokenSource() függvénynek van olyan verziója is, amelyik tokenek tömbjét veszi át, így nem csak két token forrást lehet összekapcsolni.

CancellationToken mint értéktípus

A CancellationToken értéktípusként van megvalósítva, aminek van pár érdekes következménye. Egyrészt, semmi nem tudja megakadályozni, hogy bármikor létrehozzunk egyet “csak úgy”:

CancellationToken token;
bool cancelled = token.IsCancellationRequested;

A fenti kód működik, ami felvet pár kérdést. A CancellationToken egy “vevő”, ami egy “adó”-ra (CancellationTokenSource-ra) hallgat. Most vajon mi áll a token változó mögött? Azon a módon, ahogy a token változót létrehoztuk, a CLR a változó területét kinullázta. Emiatt a token null referenciát tárol a forrásra. Mivel ez a helyzet elkerülhetetlen (a CLR értéktípusnál nem hív magától default konstruktort, így egy protected default konstruktor nem megoldás, C#-ban meg nem is lehet default konstruktort írni értéktípusokhoz), a CancellationToken függvényei erre fel is vannak készítve. Az IsCancellationRequested property például null referencia esetén azt mondja, hogy nem volt visszavonási igény. A Register függvény nem regisztrál, mivel a “semmi” úgysem fog visszavonási igénnyel élni. Van azonban olyan helyzet, amikor a CancellationToken nem tud ilyen egyszerűen csalni. Mi van akkor, amikor a WaitHandle property-t kérjük le? Lehetne gyártani egy WaitHandle-t “reptében”, de ez egy viszonylag drága művelet a semmiért. Emiatt a CancellationToken implementációja egy másik utat választ.

A CancellationTokenSource osztálynak van két előre elkészített statikus példánya. Az egyik olyan speciális állapotban van, hogy soha nem lehet triggerelni. A másik pedig már eleve triggerelt állapotban van. Amikor létrehozunk egy CancellationToken-t csak úgy magában, és egy WaitHandle-t kérünk tőle, akkor a CancellationToken hozzárendeli magát ahhoz a statikus CancellationTokenSource-hoz, amelyiket nem lehet triggerelni, és annak adja vissza a WaitHandle property-jét. Emiatt akárhány üres tokent hozunk létre, akárhányszor kérjük el a WaitHandle-jét, mindig ugyanazt az egyszem WaitHandle-t kapjuk.

Az eleve triggerelt CancellationTokenSource akkor kerül használatba, amikor egy CancellationToken-t a következő sorral hozunk létre:

var token = new CancellationToken(true);

A CancellationToken-nek tehát van egy olyan konstruktora, ahol megadhatjuk, hogy eleve visszavont, vagy soha vissza nem vonható CancellationTokenSource-ra állított tokent szeretnénk. Ennek akkor van értelme, ha egy kód várja a tokent, de az őt hívó kódig az nincs elvezetve (vagy nem a .NET 4 Cancellation Model-t használja). Összesen tehát három trükkös módon létrehozott tokennel lehet dolgunk:

  • CancellationToken token; // erről már volt szó, alapjában nincs hozzárendelve forráshoz. Viselkedésében egyébként ugyanaz, mint a new CancellationToken(true). Használhatjuk még a CancellationToken.None property-t, ami egyébként egy return new CancellationToken() hívásból áll.
  • var token = new CancellationToken(true); // ez egy triggerelhetetlen token lesz, működésében egyébként hasonló, mint az előző pont.
  • var token = new CancellationToken(false); // eleve triggerelt forráshoz tartozó token, nem tudok értelmes példát mondani, ahol használható.

Összefoglalás

Az előzőekben megismertük a .NET 4 Cancellation modeljét. Egyelőre csak kevés osztály használja a .NET Framework-ön belül, illetve saját osztályainkat már érdemes lehet ezt a modelt felhasználva megépíteni.

  1. #1 by flata on 2011. April 29. - 21:17

    Imádom a posztjaidat🙂 Csak így tovább😉

  2. #2 by Tóth Viktor on 2011. April 30. - 09:05

    Köszönöm, majd igyekszem😉

  3. #3 by eMeL on 2011. September 9. - 21:56

    Csatlakozom, közérthető mégis alapos, valódi háttértudást mutatók az írásaid.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: