neděle 31. července 2016

Poznámky z code review - čekací smyčky

Někdy je potřeba provést operaci, která může déle trvat. Navíc v případě, že se tato operace provádí delší než maximálně očekávanou dobu, je pak obvykle nutné provést nějakou další operaci - například prodloužit "zámek" nad vzkazem ve frontě apod. Obvykle se tedy spustí čekací smyčka, která hlídá nepřekročení časového limitu a je zrušena po úspěšném provedení hlavní operace.


Takovou delší operaci lze napodobit následujícím kódem:
var cancellationSource = new CancellationTokenSource();
 
WatchExeTime(InformAboutLongTime, cancellationSource.Token);
 
Console.WriteLine("Going to execute a long time operation.");
 
Thread.Sleep(16000);
cancellationSource.Cancel();
 
Console.WriteLine("The long time operation executed");
 
Console.ReadLine();

Samozřejmě, že v reálném kódu bude délka trvání operace proměnná, nastavil jsem zde napevno 16 sekund. Každé tři sekundy je přitom nutné informovat o běhu operace, to zde zastává metoda InformAboutLongTime, která jen na konzoli vypíše hlášení - opět, v reálném kódu by se volala jiná operace, například již zmíněné prodloužení zámku nad zprávou.
static private void InformAboutLongTime()
{
    Console.WriteLine("The operation in progress, please wait");
}
Vlastní hlídací metoda pak byla realizována takto:
static private void WatchExeTime(Action action, CancellationToken cancellationToken)
{
    Task.Run(
    () =>
    {
        var sw = new Stopwatch();
        sw.Start();
 
        var invokeAt = TimeSpan.FromSeconds(3);
 
        while (true)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return;
            }
 
            if (sw.Elapsed > invokeAt)
            {
                action();
                sw.Restart();
            }
        }
    },
    cancellationToken).ContinueWith(t => { }, TaskContinuationOptions.OnlyOnFaulted);
}

A co je na něm špatně? Kód ve smyčce se neustále opakuje, CPU je tak zbytečně vytížena. Pokud je tato aplikace, která vlastně nic nedělá, spuštěna, a pomocí Diagnostic Tools sledujete vytížení CPU, může tato aplikace vytížit CPU z nezanedbatelné části - bude se jednat o desítky procent:


Přitom stačí jen vhodně využít vlastnost CancellationToken a přepsat kód takto:
private static void WatchExeTime(Action action, CancellationToken cancellationToken)
        {
            Task.Run(
            () =>
            {
        var waitFor = TimeSpan.FromSeconds(3);
 
        while (cancellationToken.IsCancellationRequested == false)
                {
                    var cancelled = cancellationToken.WaitHandle.WaitOne(waitFor);

                    if (cancelled)
                    {
                        break;
                    }
 
            action();
        }
            },
           cancellationToken).ContinueWith(t => { }, TaskContinuationOptions.OnlyOnFaulted);
        }

Mimo to, že je kód kratší a tedy přehlednější a udržovatelnější, klesne hlavně i vytížení CPU - a to velmi podstatně:


Celý kód je dostupný zde a o vhodnosti tohoto přístupu se lze přesvědčit záměnou metod WatchExeTime (nevhodný přístup) a WatchExeTime2 (vhodný přístup) :

3 komentáře:

  1. Kdyz uz jsi sahnul na WaitHandle, bylo by dobre CancellationTokenSource disposnout (nebo to delat obecne vzdycky). A misto WaitHandle.WaitOne, bych to cele prepsal na Task.Delay(3000) a smycku a je to pak na par radku.

    OdpovědětVymazat
  2. s tim Dispose mas pravdu, diky. S Task.Delay to myslis nejak takto:

    Task.Run(
    async () =>
    {
    while (true)
    {
    await Task.Delay(3000, cancellationToken);
    action();
    }
    },cancellationToken)
    .ContinueWith(t => { Console.WriteLine("Wake up"); },TaskContinuationOptions.OnlyOnCanceled)
    .ConfigureAwait(false);

    OdpovědětVymazat