Poznáváme C# a Microsoft.NET – 53. díl – Timer a asynchronní delegáti

Tento díl je poslední, který se zaobírá realizací asynchronních operací v prostředí .NET. Dozvíme se, co se skrývá pod pojmem asynchronní delegát a jak je možné jej použít. Kromě toho se podíváme jak je pomocí třídy Timer možné zařídit opakování nějaké operace v určitém čase.

Asynchronní delegáti

Co jsou delegáti už bychom měli nějakou dobu vědět. V rámci .NET ale ještě existuje speciální typ delegátů, který je určen ke spouštění asociovaných metod asynchronně a tento typ delegátů je příznačně označován jako asynchronní delegáti.

Hlavní rozdíl v použití oproti nám již známým synchronním delegátům je zavolání delegáta. Pokud chceme, aby byl náš delegát spuštěn asynchronně, učiníme to zavoláním metody BeginInvoke na jeho instanci. Tím zapříčiníme, že běhové prostředí, místo toho, aby zavolala asociovanou metodu s instancí delegáta v tom samém vlákně, jak je tomu obvykle, spustí tuto metodu ve vlákně vlastním. Přesněji je použito vlákno poskytnuté komponentou ThreadPool, se kterou jsme se seznámili v minulém díle, to znamená, že metody asynchronních delegátů jsou spouštěny ve vláknech typu démon.

Jelikož delegáti můžou (a také jsou tak často vytvářeni) vracet nějaký výsledek, určitě vás napadlo, jakým způsobem je možné si tento výsledek vyzvednout, když je metoda spuštěna v jiném vlákně. K tomu musíme pomocí delegáta AsyncCallback definovat metodu, která je zavolána po dokončení práce metody asociované s asynchronním delegátem.

Následující příklad ukazuje použití asynchronního delegáta.

/// <summary>
/// Ukazka asynchroniho provadeni metod pomoci
/// asynchronich delegatu
/// </summary>
public class AsynchronousDelegates
{
  public delegate string Compute(int count);
   
  internal static void Run()
  {
    //vytvoreni instance delegate pro provadeni asynchronni operace
    Compute lCompute = new Compute(AsynMethod);
    //vytvoreni instance delegata pro zpracovani
    //vysledku asynchronni operace
    AsyncCallback lAsyncHandler = new AsyncCallback(ShowAsyncResult);
    //zavolani zpracovani metody asociovane s nasim
    //delegatem asynchronne
    lCompute.BeginInvoke(10, lAsyncHandler, "Dalsi data..");
    Console.WriteLine("Asynchronni operace byla zapocata..");
  }

  static void ShowAsyncResult(IAsyncResult result)
  {
    Thread.Sleep(1000);
    //prevzeti vysledku asynchronni operace
    AsyncResult lAsResult = (AsyncResult) result;
    //ziskani reference na naseho delegata
    Compute lDelegate = (Compute)lAsResult.AsyncDelegate;
    //ziskani hodnoty, ktera byla predana jako treti
    //parametr pri volani delegata
    string lData = (string) lAsResult.AsyncState;
    //ziskani vysledku metody
    string lResult = lDelegate.EndInvoke(lAsResult);
    Console.WriteLine("Vysledek je : {0}, Predana data : {1}", lResult, lData);
    }

  static string AsynMethod(int count)
  {
    System.Text.StringBuilder lResult = new System.Text.StringBuilder(count);
    for(int i = 0; i < count; i++)
    {
      lResult.Append(i);
    }
    return lResult.ToString();
  }
}

V příkladu jsem definoval typ delegáta Compute, který předepisuje rozhraní asociované metody na jeden vstupní parametr typu int a na návratovou hodnotu typu string. Klíčové metodě instance delegáta, která z něj učiní asynchronního delegáta, tj. metodě BeginInvoke, předáme kromě parametrů předepsaných delegátem, také referenci na instanci delegáta AsyncCallback a v případě potřeby také nějaká dodatečná data. Delegát AsyncCallback předepisuje metodu bez návratové hodnoty s jedním parametrem typu rozhraní IAsyncResult. V našem příkladu je z instancí tohoto delegáta asociována metoda ShowAsyncResult. Tato metoda je běhovým prostředím zavolána v době, kdy je práce metody asociované s asynchronním delegátem dokončena.

Jelikož je tato metoda běhovým prostředí zavolána s implementací rozhraní IAsyncResult určenou pro použití s asynchronními delegáty, kterou je třída AsyncResult, provedeme přetypování vstupního parametru za účelem přístupu ke všem členům třídy AsyncResult. Po tomto přetypování jsme schopni pomocí instanční vlastnosti AsyncDelegate získat referenci na instanci asynchronního delegáta, jehož výsledek chceme zpracovat. Získání návratové hodnoty zařídíme zavoláním metody EndInvoke na získané instanci asynchronního delegáta. V případě, že jsme při volání metody BeginInvoke, pro spuštění asynchronního volání použili nějaká dodatečná data (poslední parametr metody), můžeme si je vyzvednout pomocí vlastnosti AsyncState.

Poznámka: Volaní metody Thread.Sleep v metodě ShowAsyncResult není samozřejmě nutné a použil jsem ho jen na důkaz toho, že je volání prováděno asynchronně.

Třída Timer

Předtím, než v rámci tohoto seriálu definitivně uzavřu část věnovanou asynchronním operacím, si dovolím vás seznámit ještě s jedním zajímavým přístupem jak tyto operace realizovat. Možná se pří svém programování ve světe .NETu dostanete do situace, kdy potřebujete spustit nějakou asynchronní operaci, která bude, na rozdíl od těch, které jsem zatím popsal, v určitém časovém intervalu opakována. Hezkým příkladem této situace, je že ve vámi tvořeném emailovém klientovi, chcete v určeném intervalu kontrolovat jestli nepřišla nová pošta. Pokud se před nějakým takovýmto problémem octnete, můžete směle využit třídy Timer ze známého jmenného prostoru System.Threading. A jak na to? Například tímto způsobem:

/// <summary>
/// Ukazka pouziti tridy Timer
/// </summary>
internal class TimerExam
{
  internal static void PrintTime(object data)
  {
    Console.WriteLine("Aktualni cas je : {0}", DateTime.Now.ToString());
  }

  internal static void RunPrintingTime()
  {
    TimerCallback lTimerMethod = new TimerCallback(PrintTime);
    Timer lTimer = new Timer(lTimerMethod, null, 0, 1000);
  }
}

Jak vidíte, tak základní použití této třídy je velmi snadné. Jediné co potřebujeme je vytvoření instance delegáta TimerCallback, kterou asociujeme s metodou, jež má být opakovaně volána. Pak už stačí jen vytvořit instanci třídy Timer a kromě delegáta předat čas za který má být opakované spouštění započato a také čas intervalu pro opakování. V našem případě tedy pouštíme bez jakékoli úvodní prodlevy metodu PrintTime a ta bude následně volána každou vteřinu.

Delegát TimerCallback předepisuje metodu bez návratového typu a jeden parametr typu předka všech typů v .NET, tedy typu System.Object, který může posloužit k předání nějakých informací volané metodě. Následující příklad demonstruje použití tohoto parametru k uchování informaci o stavu časovače.

/// <summary>
/// Predstavuje stav timeru.
/// </summary>
internal class TimerState
{
  private int count;
  private int endCount;
  private Timer timer;

  /// <summary>
  /// Urcuje kolikrat byla metoda timeru spustena
  /// </summary>
  internal int Count
  {
    get{return count;}
    set{count = value;}
  }

  /// <summary>
  /// Pocet kolikrat ma byt metoda timeru vykonana
  /// </summary>
  internal int EndCount
  {
    get{return endCount;}
    set{endCount = value;}
  }

  /// <summary>
  /// Odkaz na timer (bude mozne s nim manipulovat v timer metode)
  /// </summary>
  internal Timer Timer
  {
    get{return timer;}
    set{timer = value;}
  }
}
 
internal class TimerWithStateExam
{
  internal static void RunTimer()
  {
    TimerState lState = new TimerState();
    //chceme, aby metoda byla vyvolana 5 krat
    lState.EndCount = 5;
    TimerCallback lTimerMethod = new TimerCallback(DoWork);
    //predame referenci na instanci tridy ThreadState
    Timer lTimer = new Timer(lTimerMethod, lState, 0, 1000);
    lState.Timer = lTimer;
    //dokud beh timeru ukoncen cekame
    while (lState.Timer != null)
    {
      Thread.Sleep(100);
    }
    Console.WriteLine("Priklad na timer konci..");
  }

  internal static void DoWork(object state)
  {
    TimerState lState = (TimerState) state;
    lState.Count++;
    Console.WriteLine("Aktualni stav citace je : {0}", lState.Count);
    if (lState.Count == lState.EndCount)
    {
      Console.WriteLine("Ukoncuji beh timeru");
      lState.Timer.Dispose();
      lState.Timer = null;
    }
  }
}

K uchování stavu časovače je v příkladu definována třída TimerState, jejíž instance udržují informace o tom kolikrát už byla konkrétní metoda časovačem spuštěna (vlastnost Count), při kolikátém spuštění má být běh časovače ukončen (vlastnost EndCount) a k tomu, aby mohl být časovač v metodě ukončen je potřeba na něj mít referenci (Vlastnost Timer), této vlastnosti je využito i v metodě která Timer spouští (RunTimer) a pomocí ní zjišťuje jestli ještě Timer běží.

Užitečnou metodou třídy Timer je metoda Change, kterou zapříčiníme restart Timeru po určeném čase (první parametr) a nastavíme mu interval pro opakování metody (druhý parametr). Takže pokud doplníme předchozí příklad o využití této metody, budeme schopní měnit časový interval pro volání metody DoWork.

internal class TimerStateWithPeriod
{
  ...
 
/// <summary>
  /// Cas pro opakovani
  /// </summary>
  internal int Period
  {
    get{return period;}
    set{period = value;}
  }
 
  ...
}
 
internal class TimerChangeExam
{
  internal static void RunTimer()
  {
    TimerStateWithPeriod lState = new TimerStateWithPeriod();
    lState.EndCount = 8;
    lState.Period = 1000;
    TimerCallback lTimerMethod = new TimerCallback(DoWork);
    Timer lTimer = new Timer(lTimerMethod, lState, 0, lState.Period);
    lState.Timer = lTimer;
    while (lState.Timer != null)
    {
      Thread.Sleep(100);
    }
    Console.WriteLine("Priklad na Timer konci");
  }

  internal static void DoWork(object state)
  {
    TimerStateWithPeriod lState = (TimerStateWithPeriod) state;
    lState.Count++;
    Console.WriteLine("Aktualni stav citace je : {0}", lState.Count);
    if (lState.Count == lState.EndCount)
    {
      Console.WriteLine("Ukoncuji cinnost timeru..");
      lState.Timer.Dispose();
      lState.Timer = null;
    }
    else if ( (lState.Count % 3) == 0)
    {
      int lNewPeriod = lState.Period - 250;
      Console.WriteLine("Menim cas opakovani na : {0}", lNewPeriod);
      lState.Timer.Change(lState.Period, lNewPeriod);
      lState.Period = lNewPeriod;
    }
  }
}

V příkladu byla do třídy TimerState přidána vlastnost Period, která uchovává informaci o aktuálním intervalu pro opakování a tato vlastnost je spolu s metodou Change využita v metodě DoWork, která je spouštěna časovačem a to tak, že pokud je hodnota vlastnosti Count dělitelná třemi je zkrácen interval pro opakování volání metody.

Příklady ke článku jsou ke stažení zde.

Určitě si přečtěte

Články odjinud