Machine? W tym wpisie opowiem o "Awaitables". Jeżeli pracujesz z "async i await" prawdopodobnie słyszałeś te wyrażenie nie jeden raz.  Tylko nie miałeś czasu wniknąć czym ten "Awaitable" jest.

Awaitable reprezentuje typ, na który możemy czekać i wykonać operację przy pomocy słowa kluczowego "await".

Task i Task<T> są tymi typami "awaitable".

TaskAwaiter awaiter = task.GetAwaiter();

W przypadku Taska będzie to typ : TaskAwaiter

Co tak naprawdę się dzieje, jak robisz "await". Wiemy, że w momencie użycia słowa "await" od razu startujemy nasze zadanie i czekamy na rezultat tej operacji bez blokowania głównego wątku.

static async Task Main()
{
    int a = await RandomNumberAsync();
}

private static Task<int> RandomNumberAsync()
{
    return Task.FromResult(4);
}

Jakbyś chcieli wykonać podobną operację do "await" to byśmy zrobili to tak. Z wyraźny podkreśleniem na "podobną".

static async Task Main()
{
    Task<int> awaitableType = RandomNumberAsync();

    var awaiter = awaitableType.GetAwaiter();

    if (!awaiter.IsCompleted)
    {
        //robisz operacje która nie jest związana
        //z zakończeniem tego zadania
        //coś może działać w tle
        //To symulje kod przed wyrażeniem await
    }

    int a = awaiter.GetResult();
    //kod po await

    //ten kod jest synchroniczny gdyż nie jest on pakowany przez maszynę stanów
}

private static Task<int> RandomNumberAsync()
{
    return Task.FromResult(4);
}

Ten kod jest synchroniczny, gdyż nie jest on pakowany przez maszynę stanów

Korzystamy z metody GetAwaiter(). Później możemy sprawdzić, czy operacja nie zakończyła się prawie natychmiastowo. Jeżeli tak nie jest, to znaczy, że mamy do czynienia z prawdziwą operacją asynchroniczną, która potrwa dłużej niż 2-4 milisekundy.

Pytanie 1 : Ile razy jest sprawdzana właściwość "IsCompleted" przez słowo kluczowe await?

Dodatkowo masz w nim metodę OnCompleted(). Przyjmuje ona tylko delegatę Action bez żadnych parametrów wyjściowych czy wejściowych. Nazwa tej metody sugeruje, że uruchomi się ona, gdy zadanie się zakończy...czy jednak tak jest.

Pytanie  2 : Czy metoda OnCompleted() zawsze się uruchamia?

Zaraz na te pytania odpowiedzmy kodem.

Task<int> awaitableType = RandomNumberAsync();
var awaiter = awaitableType.GetAwaiter();

awaiter.OnCompleted(
    () => { Console.WriteLine("Koniec"); }
);

Na czym możemy wywołać "await"? 

Otóż możesz użyć słowa kluczowego "await" na wszystkim, co ma metodę "GetAwaiter()". Ta metoda może być z instancji bądź z metody rozszerzeniowej.

Obiekt zwracany przez metodę GetAwaiter() musi mieć

  • Implementacje INotifyCompletion i metodę "OnCompleted".
  • Posiadać pole IsCompleted
  • Posiadać metodę "GetResult()"

Tak właśnie Task i Task<T> także działa. 

TaskAwaiter awaiter = task.GetAwaiter();

Stwórzmy więc prostą klasę :

public class MyAwaitable
{
    public MyAwaiter GetAwaiter() => new MyAwaiter();
}
public class MyAwaiter : INotifyCompletion
{
    public void OnCompleted(Action continuation)
    {
        Console.WriteLine("Przed OnCompleted");
        continuation.Invoke();
        Console.WriteLine("Po OnCompleted");
    }

    public bool IsCompleted
    {
        get
        {
            return true;
        }
    }

    public string GetResult()
    {
        return "Done";
    }
}

Jak on w praktyce będzie działał?

static async Task Main()
{

    var s = await new MyAwaitable();
    Console.WriteLine(s);
}

W swoim obiekcie "MyAwaiter" zwracam natychmiastowo wartość true dla właściwości IsCompleted. W tym przypadku nie uruchomi się metoda "OnCompleted".

Ciekawe

Spróbowałem, też sprawdzić jak działa właściwość IsCompleted. Moje pierwsze wyobrażenie o działaniu "async i await" mówiło mi, że zapewne ta właściwość jest sprawdzana raz na jakiś czas w jakieś pętli while. Z drugiej strony to brzmi bardzo nie wydajnie.

Napisałem taki szalony kod, który zwróci w jakimś losowym przypadku wartość "true". To powinno potwierdzić, ile razy metoda IsCompleted jest wywoływana.

public class MyAwaiter2 : INotifyCompletion
{
    public void OnCompleted(Action continuation)
    {
        Console.WriteLine("Przed OnCompleted");
        continuation.Invoke();
        Console.WriteLine("Po OnCompleted");
    }

    private bool _isCompleted = false;
    public bool IsCompleted
    {
        get
        {
            if (_isCompleted)
                return _isCompleted;

            Random r = new Random();
            var number = r.Next(0, 100);

            if (number > 90)
            {
                _isCompleted = true;
                return true;
            }
            return false;
        }
    }

    public string GetResult()
    {
        return "Done";
    }
}

Jednakże właściwość IsCompleted jest sprawdzana przez mechanizm w maszyny stanów w C# tylko raz.

Gdy zajrzymy jak maszyna stanów "async i await" to się wyjaśni dlaczego.

IsCompleted więc służy do sprawdzenia, czy dana operacja wykonała się JUŻ (natychmiastowo i synchroniczne).

Wiemy więc, że C# nie robi czegoś takiego:

var awaiter = new MyAwaitable().GetAwaiter();

while (!awaiter.IsCompleted)
{
    Console.WriteLine("Czekam!");
    Thread.Sleep(500);
}

Ten kod natomiast pokazuje jak działa metoda OnCompleted(). Jaki ona ma cel? Nazwa sugeruje, że powinna się ona uruchamiać, gdy nasze zadanie się zakończy.

Skoro właściwość IsCompleted jest przez C# sprawdzana tylko raz to mogę permanentnie ją ustawić na false.

public class MyAwaiter3 : INotifyCompletion
{
    public void OnCompleted(Action continuation)
    {
        Console.WriteLine("Przed OnCompleted");
        continuation.Invoke();
        Console.WriteLine("Po OnCompleted");
    }

    private bool _isCompleted = false;
    public bool IsCompleted
    {
        get
        {
            return false;
        }
    }

    public string GetResult()
    {
        Thread.Sleep(4000);
        return "Done";
    }
}

Metoda OnCompleted() teraz się wykona, ponieważ początkowe sprawdzenie "IsCompleted" potwierdziło C#-powi, że jest to niby prawdziwa operacja asynchroniczna, która wymaga dalszego działania. 

Później zobaczymy maszynę stanów "async i await" i to się wyjaśni, dlaczego tak się dzieje.

Pytanie 3 : Co dokładnie symbolizuje delegata Action "continuation".

Aby napisać dobrego Awaitera, najpierw musimy odpowiedzieć sobie na to pytanie. 

Ta delegata ma reprezentować kod, który ma się wykonać po udanej operacji. Bawiąc się kodem, zauważyłem, że jeśli nie zrobimy wywołania tej delegaty "continuation" to cała aplikacja się zatrzyma. Dotyczy to wyrażenia "async i await", a nie,  synchronicznego wyrażenia bez maszyny stanów "GetAwaiter().GetResult()"

Swoją drogą jak zauważyłeś metoda GetResult() w tych przykładach zwraca "string", ale zaraz przecież rezultatem akcji asynchronicznej mogą być nie tylko napisy.

public string GetResult()
{
    Thread.Sleep(4000);
    return "Done";
}
public int GetResult()
{
    Thread.Sleep(4000);
    return 1;
}

Jak się okazuje typ zwracany przez GetResult, jest typem dynamicznym i może być w twoim "Awaiter" czymkolwiek.

Teraz gdy to wszystko wiemy, warto się zastanowić jak prawdziwie użyteczny "Awaiter" napisać. Najłatwiej by było skorzystać z gotowego TaskAwaiter i otoczyć go swoim kodem.

Jednak tutaj chcemy się nauczyć jak taki Awaiter napisać dobrze od zera. Wszystkie poprzednie przykłady są niekompletne i tylko nauczyły nas jak maszyna stanów "async i await" działa.

Lepsza klasa "Awaitable" otwiera oczy na mechanizmy async i await

Tutaj zaczynają się na serio smoki. 

public class MyAwaitable4
{
    private volatile bool finished;
    public bool IsFinished => finished;
    public event Action Finished;
    public MyAwaitable4(bool finished) => this.finished = finished;
    public void TryFinish()
    {
        if (finished) return;

        Random r = new Random();
        var number = r.Next(0, 100);

        if (number > 95)
        {
            finished = true;
            Finished?.Invoke();
        }

    }
    public MyAwaiter4 GetAwaiter() => new MyAwaiter4(this);
}

Musiałby, by mieć ona jakąś flagę informującą nas, że zadania się wykonało. Na razie nie będę wyjaśniał, co robi słowo kluczowe "volatile". Będzie na to okazja w innym wpisie.

Kluczowa jest tutaj metoda TryFinish(). Ma ona 5% szansy na zakończenie działania tego asynchronicznego zadania.

Zdarzenie IsFinished uruchomi dalszą część kodu i będzie ona potrzebna do uruchomienia delegaty "continuation" przy wyrażeniu kluczowym "await"

Nasz Awaiter wygląda tak. 

public class MyAwaiter4 : INotifyCompletion
{
    private readonly MyAwaitable4 awaitable;
    private int result;

    public MyAwaiter4(MyAwaitable4 awaitable)
    {
        this.awaitable = awaitable;
        if (IsCompleted)
            SetResult();

    }
    public bool IsCompleted => awaitable.IsFinished;

    public int GetResult()
    {
        if (!IsCompleted)
        {
            //var wait = new SpinWait();
            while (!IsCompleted)
            {
                Console.WriteLine("Czekam SPIN");
                awaitable.TryFinish();
                //wait.SpinOnce();
                Thread.Sleep(100);
            }

        }
        return result;
    }

    public void OnCompleted(Action continuation)
    {
        if (IsCompleted)
        {
            continuation();
            return;
        }
        var capturedContext = SynchronizationContext.Current;
        awaitable.Finished += () =>
        {
            SetResult();
            if (capturedContext != null)
                capturedContext.Post(_ => continuation(), null);
            else
                continuation();
        };
        GetResult();
    }

    private void SetResult()
    {
        result = new Random().Next();
    }
}

Wszystko wygląda skomplikowanie. Jak tego użyć? Oczywiście, że tak:

static async Task Main()
{
    var a = new MyAwaitable4(false);
    var awaiter = a.GetAwaiter();
    awaiter.OnCompleted(() => { Console.WriteLine("Dalsza część kodu"); });
    var res = awaiter.GetResult();
    Console.WriteLine(res);

    var b = new MyAwaitable4(false);
    var res2 = await b;
    //dalsza część kodu (dosłownie)
    Console.WriteLine(res2);

}

Teraz warto odświeżyć wiedze. "a.GetAwaiter().GetResult()" i słowo kluczowe await to nie dokładnie te same operacje.

W swojej maszynie stanów "async i await" ma miejsce na wykonanie "a.GetAwaiter().GetResult()".

Pamiętaj a.GetAwaiter().GetResult() jest synchroniczny, więc blokuje wątek główny. 

Pisząc swój "Awaiter" widzę, że jestem zmuszony  napisać wykonanie "GetResult()" w metodzie OnCompleted() i utworzyć delegatę, która wykona dalszy kod.

public void OnCompleted(Action continuation)
{
    if (IsCompleted)
    {
        continuation();
        return;
    }
    var capturedContext = SynchronizationContext.Current;
    awaitable.Finished += () =>
    {
        SetResult();
        if (capturedContext != null)
            capturedContext.Post(_ => continuation(), null);
        else
            continuation();
    };
    GetResult();
}

Kto wie, może TaskAwaiter robi dokładnie to samo.

Będę co 100 milisekund próbował zakończyć ten proces i powiadomić kontekst delegatą o tym, że reszta kodu może być uruchamiana.

public int GetResult()
{
    if (!IsCompleted)
    {
        //var wait = new SpinWait();
        while (!IsCompleted)
        {
            Console.WriteLine("Czekam SPIN");
            awaitable.TryFinish();
            //wait.SpinOnce();
            Thread.Sleep(100);
        }

    }
    return result;
}

Naprawdę w TryFinish() bardzo ważne jest to, że uruchomi się delegata Finshed(), a ona obsłuży delegatę "continuation()". Jeżeli tak nie będzie to aplikacja nie uruchomi nic dalej po wyrażeniu "await". Widać jest to krytyczne dla działania maszyny stanu stworzonego przez wyrażenie "async i await".

Delegata Action continuation naprawdę reprezentuje dalszy wykonywany kod, gdy korzystamy z wyrażenia kluczowego "async i await".

public void TryFinish()
{
    if (finished) return;


    Random r = new Random();
    var number = r.Next(0, 100);

    if (number > 95)
    {
        finished = true;
        Finished?.Invoke();
    }

}

Ten przykład mógłby być jeszcze lepszy. Trzeba by było dodać jeszcze obsługę ConfigureAwait. Przydałaby się też obsługa braku SynchronizationContext.

Na szczęście w swojej karierze nie będziesz musiał pisać takiego kodu. Wystarczy się otoczyć gotowym TaskAwaiter, aby stworzyć swoje zachowanie "Awaitable".

Sensowne użycie awaitable z TaskAwaiter

Tak się składa, że ten programista Stephen w 2011 roku bardzo dobrze wyczerpał ten temat. Oto jego genialny wpis na blogu

https://devblogs.microsoft.com/pfxteam/await-anything/

Można napisać swój awaiter, który uruchomi danego Taska w innej kulturze.

public static CultureAwaiter WithCurrentCulture(this Task task)
{
    return new CultureAwaiter(task);
}

public class CultureAwaiter : INotifyCompletion
{
    private readonly TaskAwaiter m_awaiter;
    private CultureInfo m_culture;

    public CultureAwaiter(Task task)
    {
        if (task == null) throw new ArgumentNullException(“task”);
        m_awaiter = task.GetAwaiter();
    }

    public CultureAwaiter GetAwaiter() { return this; }

    public bool IsCompleted { get { return m_awaiter.IsCompleted; } }

    public void OnCompleted(Action continuation)
    {
        m_culture = Thread.CurrentThread.CurentCulture; 
        m_awaiter.OnCompleted(continuation);
    }

    public void GetResult()
    {
        if (m_culture != null) Thread.CurrentThread.CurrentCulture = m_culture; 

        m_awaiter.GetResult();
    }
}

Stworzył on też przykład "awaiter" dla kontrolek w WPF:

public static ControlAwaiter GetAwaiter(this Control control)
{
    return new ControlAwaiter(control);
}

public struct ControlAwaiter : INotifyCompletion
{
    private readonly Control m_control;

    public ControlAwaiter(Control control)
    { 
        m_control = control;
    }

    public bool IsCompleted
    { 
        get { return !m_control.InvokeRequired; }
    }

    public void OnCompleted(Action continuation)
    { 
        m_control.BeginInvoke(continuation); 
    }

    public void GetResult() { }
}

Jak to jest z metodami rozszerzeniowymi "GetAwaiter()".

Nic nie stoi na przeszkodzie, aby napisać taki kod.

public static class TaskAwaiterHelper 
{
    public static TaskAwaiter GetAwaiter(this TimeSpan timespan)
    {
        return Task.Delay(timespan).GetAwaiter();
    }

    public static TaskAwaiter GetAwaiter(this string word)
    {
        return Task.Delay(word.Length).GetAwaiter();
    }

    public static TaskAwaiter GetAwaiter(this DateTimeOffset dateTimeOffset)
    {
        return (dateTimeOffset – DateTimeOffset.UtcNow).GetAwaiter();
    }
}

Potem go użyć tak. 

static async Task Main()
{
    await TimeSpan.FromSeconds(2);
    await "Stefan";
}

Wygląda to abstrakcyjnie, ale szczerze w tym momencie ogranicza Cię wyobraźnia. 

Pokaż jakiś fajny bajer z TaskCompletionSource 

Korzystając z TaskCompletionSource, możesz opakować pewne działania, które nie zostały stworzone z myślą o "async i await".

Przykładowo zdarzyło mi się w swojej karierze uruchamiać program/proces antywirusowy i czeka na jego "exitcode" który informował mnie czy dany plik był wirusem, czy nie.

Na potrzeby tego wpisu załóżmy, że tym programem jest "notepad.exe". Mój kod wyglądał tak:

static async Task Main()
{
    var proces = Process.Start("notepad.exe");

    proces.WaitForExit();

    var result = proces.ExitCode;
}

Uruchomienie procesu i jego zamkniecie jest w pewnym sensie operacją asynchroniczną tylko jak to jakoś zapakować do typu "awaitable". Mógłbym skorzystać zdarzenia Exited(), ale to jest staro modne. Trzeba by było te zdarzenie Exited() zapakować do typu awaitable.

Normalnie nie byłoby to możliwe, ale z pomocą przychodzi TaskCompletionSource. Jest to pomocna klasa to opakowywania starego API zdarzeniowego do Tasków, na których można zrobić GetAwaiter()

public static class TaskAwaiterHelper
{
    public static TaskAwaiter<int> GetAwaiter(this Process process)
    {
        var tsc = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);

        process.EnableRaisingEvents = true;
        process.Exited += (sender, args) =>
        {
            var senderProcess = sender as Process;

            if (senderProcess == null)
                return;

            tsc.SetResult(senderProcess.ExitCode);
        };

        return tsc.Task.GetAwaiter();
    }
}

Jak widzisz, TaskCompletionSource posiada w sobie odpowiednie metody, aby zasymulować stworzenie Taska.

Dodatkowo trzeba skorzystać z opcji : TaskCreationOptions.RunContinuationsAsynchronously. Domyślne każda metoda kontynuowana pod takie Taski będą synchroniczne. Nie chcesz mieć takiej niespodzianki. Ta opcja też ratuje Cię przed pewnymi Deadlockami stworzonymi pod wpływem mieszania asynchronicznego kodu z synchronicznym. 

Używamy metody "SetResult" do ustawienia rezultatu, gdy nasz proces wywoła zdarzenie "Exited".

Masz w TaskCompletionSource  metody do tworzenia innych stanów Taska.

  • SetException() : Ustawia błędne zakończenie zadania
  • SetCanceled() : Ustawia stan zadania jako odwołane

Korzystając z TaskCompletionSource możesz opakować każde stare API oparte na zdarzeniach

public static Task PerformOperation(this PictureBox pictureBox)
{
    var tcs = new TaskCompletionSource<object>();
            
    // Naive version that does not unsubscribe from the event
    pictureBox.LoadCompleted += (s, ea) =>
    {
        if (ea.Cancelled) tcs.SetCanceled();
        else if (ea.Error != null) tcs.SetException(ea.Error);
        else tcs.SetResult(null);
    };
 
    pictureBox.LoadAsync();
 
    return tcs.Task;
}

Sam TaskCompletionSource często był używany w testach jednostkowych. Przed .NET 4.5 nie było metody Task.FromResult i to tworzyło taki mankament pisania MOCK-ów. Zresztą w bibliotece Moq istnieje metoda ReturnAsync,  więc ten problem obecnie nie powinien Cię spotkać.

Maszyna stanów : A co się dzieje w jak robię await Task

Mimo iż omówiliśmy dokładny mechanizm "Awaitables" to rzeczywiście nadal nie mogliśmy zobaczyć mechanizmu maszyny stanów.

Na razie to mogliśmy zobaczyć jakie metody, właściwości ta maszyna uruchamia gdzieś pod abstrakcyjnym stołem.

Gdy piszemy swój "Awaiter" tej maszyny nie widzimy.

Czym jest ta maszyna stanów.

Ten wpis : https://www.markopapic.com/csharp-under-the-hood-async-await/ 

...robi naprawdę dobrą robotę.

static async Task BarAsync()
{
    Console.WriteLine("This happens before await");

    int i = await QuxAsync();

    Console.WriteLine("This happens after await. The result of await is " + i);
}

Ten kod zostanie prze-kompilowany na coś takiego :

private static Task BarAsync()
{
  Program.<BarAsync>d__2 stateMachine;
  stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
  stateMachine.<>1__state = -1;
  stateMachine.<>t__builder.Start<Program.<BarAsync>d__2>(ref stateMachine);
  return stateMachine.<>t__builder.Task;
}

Jak widzisz, masz maszynę stanów, która na samym początku ma -1. Później wygenerowana maszyna stanów zrobi metodę MoveNext()

private struct <BarAsync>d__2 : IAsyncStateMachine
{
  public int <>1__state;
  public AsyncTaskMethodBuilder <>t__builder;
  private TaskAwaiter<int> <>u__1;

  void IAsyncStateMachine.MoveNext()
  {
	int num1 = this.<>1__state;
	try
	{
	  TaskAwaiter<int> awaiter;
	  int num2;
	  if (num1 != 0)
	  {
		Console.WriteLine("This happens before await");
		awaiter = Program.QuxAsync().GetAwaiter();
		if (!awaiter.IsCompleted)
		{
		  this.<>1__state = num2 = 0;
		  this.<>u__1 = awaiter;
		  this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<BarAsync>d__2>(ref awaiter, ref this);
		  return;
		}
	  }
	  else
	  {
		awaiter = this.<>u__1;
		this.<>u__1 = new TaskAwaiter<int>();
		this.<>1__state = num2 = -1;
	  }
	  Console.WriteLine("This happens after await. The result of await is " + (object) awaiter.GetResult());
	}
	catch (Exception ex)
	{
	  this.<>1__state = -2;
	  this.<>t__builder.SetException(ex);
	  return;
	}
	this.<>1__state = -2;
	this.<>t__builder.SetResult();
  }

  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
  {
	this.<>t__builder.SetStateMachine(stateMachine);
  }
}

W metodzie MoveNext jest wywoływana właściwość "IsCompleted". Jeżeli zwróci ona "true" to znaczy, że zadanie się już wykonało. W takim wypadku stan tej maszyny przechodzi na liczbę 0.

Później ponownie jest wykonywana metoda MoveNext. W każdej rekursji metody MoveNext jest sprawdzany stan. Jeśli jest on równy zeru, to cała ta operacja się kończy. Kończy się ona poprzez wywołanie metody "SetResult()" i stawienie stanu na -2.

Maszyna stanów też może zakończyć swój proces, gdy pojawi się wyjątek.  Jeżeli się tak stanie to jest wywoływana metoda "SetException()". Stan przechodzi na liczbę -2.

Przepływ maszyny stanów i jego stanu

Wróćmy jednak do początku.

Na początku, gdy właściwość IsCompleted zwróci wartość "false" i mamy stan początkowy -1 to wtedy 

  • Ustawiamy stan na 0, aby nie wykonać tej operacji ponownie
  • Tworzymy delegatę zwrotną w "AwaitUnsafeOnCompleted". Ta delegata uruchomi ponownie MoveNext tylko ze stanem równym zero. Delegat uruchomi się, gdy nasze zadanie asynchroniczne się wykona.

Później ponownie jest wykonywana metoda MoveNext. W każdej rekursji metody MoveNext jest sprawdzany stan. Jeśli jest on równy zero, to cała ta operacja się kończy. Kończy się ona poprzez wywołanie metody "SetResult()" i stawienie stanu na -2.

Diagram maszyny stanów async i await

Taka małe powtórne wywołanie wcale nie ułatwia zrozumienia tego ja ta maszyna stanów działa. 

Przynajmniej teraz wiesz dlaczego "IsCompleted" dla "Awaitables" jest uruchamiane tylko raz i jaką tak naprawdę rolę spełnia. 

Task.Yield

Patrząc na to, jak działa maszyna stanów i typy "Awaitables" to powstaje pewien problem. Czasami chciałbyś mieć pewność, że dalsze operacje po wykonaniu danego zadania na pewno też będą wykonywane asychroniczne

Jakbyś takie zachowanie wymusił? 

Czyli coś takiego :

public struct YieldAwaiter : INotifyCompletion
{
    public void OnCompleted(Action continuation)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback
            (
                (a) => { continuation(); }
            ));
    }

    public bool IsCompleted
    {
        get
        {
            return false;
        }
    }

    public void GetResult()
    {

    }
}

Takich przypadków jest mało, ale może chciałbyś mieć taką opcję.

Nie musisz pisać swojego "Awaiter-a", aby mieć takie zachowanie. Korzystając z Task.Yield możesz asynchronicznie wrócić do kontekstu aplikacji.

Task.Yelid obrazek z książki

Task.Yield zwraca gotowego "Awaiter-a" do takiego celu. Wygląda on tak:

public struct YieldAwaitable
{
    public YieldAwaiter GetAwaiter() { return new YieldAwaiter(); }
    
    [HostProtection(Synchronization = true, ExternalThreading = true)]
    public struct YieldAwaiter : ICriticalNotifyCompletion
    {
    
        public bool IsCompleted { get { return false; } } // yielding is always required for YieldAwaiter, hence false
    
        [SecuritySafeCritical]
        public void OnCompleted(Action continuation)
        {
            QueueContinuation(continuation, flowContext: true);
        }
    
        [SecurityCritical]
        public void UnsafeOnCompleted(Action continuation)
        {
            QueueContinuation(continuation, flowContext: false);
        }
    
        [SecurityCritical]
        private static void QueueContinuation(Action continuation, bool flowContext)
        {
            // Validate arguments
            if (continuation == null) throw new ArgumentNullException("continuation");
            Contract.EndContractBlock();
    
            if (TplEtwProvider.Log.IsEnabled())
            {
                continuation = OutputCorrelationEtwEvent(continuation);
            }
            // Get the current SynchronizationContext, and if there is one,
            // post the continuation to it.  However, treat the base type
            // as if there wasn't a SynchronizationContext, since that's what it
            // logically represents.
            var syncCtx = SynchronizationContext.CurrentNoFlow;
            if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
            {
                syncCtx.Post(s_sendOrPostCallbackRunAction, continuation);
            }
            else
            {
                // If we're targeting the default scheduler, queue to the thread pool, so that we go into the global
                // queue.  As we're going into the global queue, we might as well use QUWI, which for the global queue is
                // just a tad faster than task, due to a smaller object getting allocated and less work on the execution path.
                TaskScheduler scheduler = TaskScheduler.Current;
                if (scheduler == TaskScheduler.Default)
                {
                    if (flowContext)
                    {
                        ThreadPool.QueueUserWorkItem(s_waitCallbackRunAction, continuation);
                    }
                    else
                    {
                        ThreadPool.UnsafeQueueUserWorkItem(s_waitCallbackRunAction, continuation);
                    }
                }
                // We're targeting a custom scheduler, so queue a task.
                else
                {
                    Task.Factory.StartNew(continuation, default(CancellationToken), TaskCreationOptions.PreferFairness, scheduler);
                }
            }
        }
    
        private static Action OutputCorrelationEtwEvent(Action continuation)
        {
            int continuationId = Task.NewId();
            Task currentTask = Task.InternalCurrent;
            // fire the correlation ETW event
            TplEtwProvider.Log.AwaitTaskContinuationScheduled(TaskScheduler.Current.Id, (currentTask != null) ? currentTask.Id : 0, continuationId);
    
            return AsyncMethodBuilderCore.CreateContinuationWrapper(continuation, () =>
            {
                var etwLog = TplEtwProvider.Log;
                etwLog.TaskWaitContinuationStarted(continuationId);
    
                // ETW event for Task Wait End.
                Guid prevActivityId = new Guid();
                // Ensure the continuation runs under the correlated activity ID generated above
                if (etwLog.TasksSetActivityIds)
                    EventSource.SetCurrentThreadActivityId(TplEtwProvider.CreateGuidForTaskID(continuationId), out prevActivityId);
    
                // Invoke the original continuation provided to OnCompleted.
                continuation();
                // Restore activity ID
    
                if (etwLog.TasksSetActivityIds)
                    EventSource.SetCurrentThreadActivityId(prevActivityId);
    
                etwLog.TaskWaitContinuationComplete(continuationId);
            });
            
        }
    
        private static void RunAction(object state) { ((Action)state)(); }
    
        /// <summary>Ends the await operation.</summary>
        public void GetResult() {} // Nop. It exists purely because the compiler pattern demands it.
    }
}

Najbardziej interesuje Cię fakt, że IsCompleted jest domyślnie ustawione na false.

Najważniejszy mechanizm żyje tutaj:

[SecurityCritical]
private static void QueueContinuation(Action continuation, bool flowContext)
{
    ................
    var syncCtx = SynchronizationContext.CurrentNoFlow;
    if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
    {
        syncCtx.Post(s_sendOrPostCallbackRunAction, continuation);
    }
    else
    {
        TaskScheduler scheduler = TaskScheduler.Current;
        if (scheduler == TaskScheduler.Default)
        {
            if (flowContext)
            {
                ThreadPool.QueueUserWorkItem(s_waitCallbackRunAction, continuation);
            }
            else
            {
                ThreadPool.UnsafeQueueUserWorkItem(s_waitCallbackRunAction, continuation);
            }
        }
        // We're targeting a custom scheduler, so queue a task.
        else
        {
            Task.Factory.StartNew(continuation, default(CancellationToken), TaskCreationOptions.PreferFairness, scheduler);
        }
    }
}

Jak widzisz dalsze działanie aplikacji, jest wysłane dalej w zależności od tego, czy mamy kontekst aplikacji i czy mamy TaskScheduler.

static async Task Main()
{
    await Task.Yield();
    //dalszy kod
}

Okej więc Task.Yield magicznie sprawi, że kod będzie asynchroniczny. A przynajmniej ten kod wracający do wywoływacza Task.Yield i nie dalej.

Tworzy to jednak więcej problemów niż rozwiązań.

Ogólnie Task.Yield prawie nie ma zastosowań, ale o zgrozo czasem nawet StackOverflow sugeruje, aby go używać, aby ulepszyć - responsywnie UI w WPF czy Windows Forms.

Wiadomo więcej asynchroniczności . Mniej blokowania wątku głównego UI to znaczy, że coś będzie działało szybciej.

Niestety Task.Yield takiej roli nie spełni. Częściowe mechanizmy wątku UI mają wyższe priorytet więc Task.Yield, nie zrobi nic. 

Inne mechanizmy wątku UI, które mają niższy priorytety (jak reakcje na użytkownika)  w wyniku działania "Task.Yield" będą one blokowane.

public async void MyButton_Click(object sender, RoutedEventArgs e)
{
    for( int i=0; i < 10000; i++)
    {
        ProcessSomeStuff(i);

        // await the Yield to ensure all waiting messages
        // are processed before continuing
        await Task.Yield();
    }
}

Będziemy wrzucać w kolejkę zdarzeń coś co naszym zdaniem jest ważniejsze niż reakcje użytkownika. To brzmi jak bardzo kiepski pomysł. Spójrz na ten kod:

async void Form_Load(object s, object e) 
{ 
    await Task.Yield(); 
    MessageBox.Show("Async message!");
}

Form_Load wróci do swojego wywoływacza, czyli jakiegoś kodu w WindowsForms, który uruchomił zdarzenie Load. Później MessageBox pokaże się asynchronicznie gdzieś w przyszłej iteracji pętli zdarzeń, która wykonuje Application.Run.

W tym wypadku kontekst "WinFormsSynchronizationContext.Post" wyśle wiadomość do wątku UI, który ma swoją pętlę zdarzeń.  Ta wiadomość będzie wymuszona i kto wie, co to zablokuje po drodze i mimo wszystko wykona się na tym samym wątku.

Dobra lektura na ten temat tutaj jest  : https://stackoverflow.com/questions/23431595/task-yield-real-usages

Gdybyś chciał coś takiego jednak osiągnąć to w WPF masz do dyspozycji : Dispatcher.Yield

async Task DoUIThreadWorkAsync(CancellationToken token)
{
    var i = 0;

    while (true)
    {
        token.ThrowIfCancellationRequested();

        await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);

        // do the UI-related work item
        this.TextBlock.Text = "iteration " + i++;
    }
}

W Windows Form mógłbyś skorzystać z TaskCompletionSource i stworzyć interpretacje takiego zachowania na podstawie zdarzenia Application.Idle

public static Task IdleYield()
{
    var idleTcs = new TaskCompletionSource<bool>();
    // subscribe to Application.Idle
    EventHandler handler = null;
    handler = (s, e) =>
    {
        Application.Idle -= handler;
        idleTcs.SetResult(true);
    };
    Application.Idle += handler;
    return idleTcs.Task;
}

Rekomenduje się nie robić taki rzeczy za często. W każdej iteracji warto nie przekroczyć 50 milisekund na operacje w tle.

Raczej w swojej karierze nie użyjesz Task.Yield i jego zbliżonych mechanizmów. Chyba że masz styczność z tym BUG-iem w ASP.NET

https://stackoverflow.com/questions/16653308/why-is-an-await-task-yield-required-for-thread-currentprincipal-to-flow-corr