Thread.Sleep vs Task.Delay

Kiedyś tworzenie klasy Thread było najpopularniejszym sposobem na współbieżne wykonanie jakiegoś kodu. Niestety bardzo często było to wykorzystywane w złym kontekście – np. operacje niezbyt długie były wykonywane na wątku z klasy Thread, zamiast na tych z puli. Na szczęście dzisiaj klasa Task jest dużo bardziej popularniejsza i przełączanie się pomiędzy wątkami z puli a tworzeniem nowych jest dość łatwe.

Złą stroną jest fakt, że Task.Delay czasami jest mylony z funkcją Sleep. Porównajmy dwa fragmentu kodu:

private static void Test()
{
  Console.WriteLine("Before Sleep");
  Thread.Sleep(5000);
  Console.WriteLine("After Sleep");

  Console.WriteLine("Before Delay");
  Task.Delay(5000);
  Console.WriteLine("After Delay");
}

Czy powyższe przykłady mają taki sam efekt końcowy? Zdecydowanie nie… Sleep jak dobrze wiemy, po prostu usypia aktualny wątek, co spowoduje, że 5 sekund minie pomiędzy wyświetleniem tekstu “Before Sleep” a “After Sleep”. W przypadku Delay, zajrzyjmy najpierw do dokumentacji:

public static Task Delay(
    int millisecondsDelay
)

Funkcja zwraca nowy wątek. Oznacza to, że nie blokuje ona wykonywania i z linii “Before delay”, przejdziemy natychmiast do “After delay”. Task.Delay zachowuje się jak timer. Tworzy wątek, który będzie wykonywał się przez określony czas. Jeśli chcemy, aby efekt końcowy wyglądał podobnie do Thread.Sleep, należy:

Console.WriteLine("Before Delay");
await Task.Delay(5000);
Console.WriteLine("After Delay")

Zaglądając do Reflector’a, faktycznie dowiemy się, że Delay jest oparty na timer:

public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken)
{
    if (millisecondsDelay < -1)
    {
        throw new ArgumentOutOfRangeException("millisecondsDelay", Environment.GetResourceString("Task_Delay_InvalidMillisecondsDelay"));
    }
    if (cancellationToken.IsCancellationRequested)
    {
        return FromCancellation(cancellationToken);
    }
    if (millisecondsDelay == 0)
    {
        return CompletedTask;
    }
    DelayPromise state = new DelayPromise(cancellationToken);
    if (cancellationToken.CanBeCanceled)
    {
        state.Registration = cancellationToken.InternalRegisterWithoutEC(delegate (object state) {
            ((DelayPromise) state).Complete();
        }, state);
    }
    if (millisecondsDelay != -1)
    {
        state.Timer = new Timer(delegate (object state) {
            ((DelayPromise) state).Complete();
        }, state, millisecondsDelay, -1);
        state.Timer.KeepRootedWhileScheduled();
    }
    return state;
}

Jak widać, kod odpala timer, a następnie w nim ustawia rezultat dla wątku (tutaj DelayPromise). Fizycznie nie jest tworzony dodatkowy wątek (można to sprawdzić w Visual Studio za pomocą debugger’a). Korzystamy ze zwykłego timer’a (który jest funkcją systemu operacyjnego), a następnie zwracamy wynik opakowany po prostu w Task.

Task.Delay jest przydatny tak samo jak Sleep – czyli rzadko, głównie dla szybkich testów. Jeśli mamy podejrzenia, że jakiś bug przytrafia się tylko w określonych sytuacjach, często staramy się skorzystać  z Thread.Sleep, aby zasymulować różne operacje.

Podobnie jest z Task.Delay, z tym, że zwykle jest wykorzystywany w połączeniu z metodami async,await. Rozważmy jakąś metodę async:

private static async Task Test()
{
  // await - jakas dluga operacja
  Thread.Sleep(5000);
  // await - jakas inna dluga operacja
}

Thread.Sleep po prostu zablokuje wątek na ileś sekund. Jeśli chcemy mieć zachowanie asynchroniczne, powinniśmy skorzystać z Delay:

private static async Task Test()
{
  // await - jakas dluga operacja
  await Task.Delay(5000);
  // await - jakas inna dluga operacja
}

Zwykle, jeśli korzystamy z API opartego na async/await, wtedy wywołujemy Task.Delay, w przeciwnym wypadku Thread.Sleep. Inną ciekawostką o Delay jest możliwość anulowania zadania:

public static Task Delay(
    TimeSpan delay,
    CancellationToken cancellationToken
)

Leave a Reply

Your email address will not be published.