Jak działa Async i Await

Cezary Walenciuk

Jak działa Async i Await

@walenciukC

Speaker
Async i Await Task API

Agenda

Agenda

Agenda

Task, Async.
Katastrofalny błąd

Task, Async.

                    

                        static async Task Main()
                        {
                            await What();
                        }
                
                        static Task What()
                        {
                            return new Task(
                                () => Console.WriteLine("1"));
                
                        }
                    
                

Task, Async.

                    
                        await Task.Run
                        (
                            () => DoExpensiveOperation(someParameter)
                        );
                    
                

Task, Async.

                    

                        static async Task Main()
                        {
                            Task t = What();
                            await t;
                
                            var s = t.Status;
                        }
                
                        static Task What()
                        {
                            return new Task(
                                () => Console.WriteLine("1"));
                        }
                    
                

async eliding

async eliding

                    
                        static Task<int> M1()
                        {
                            return Task.FromResult(1);
                        }
                            
                        static async Task<int> M2()
                        {
                            return await M1();
                        }
                    
                

Przekazywanie Tasków wyżej wydaje się dobrym pomysłem, ale nie zawsze

Eliding Async and Await : PROBLEM

                    
                        public async Task<string> GetWithKeywordsAsync(string url)
                        {
                            using (var client = new HttpClient())
                                return await client.GetStringAsync(url);
                        }
                            
                        public Task<string> GetElidingKeywordsAsync(string url)
                        {
                            using (var client = new HttpClient())
                                return client.GetStringAsync(url);
                        }
                    
                

Wykona się Dispose w HttpClient więc zwrócony Task nie będzie mógł się wykonać

Async i Await : maszyna stanów pamięta zakończenie

                    

                        static async Task Main()
                        {
                            Task<string> t = 
                            File.ReadAllTextAsync(@"D:\numbers2.txt");

                            await t;
                            await t;
                            await t;
                            await t;
                        }
                    
                

Głęboki skok w async i await
Absolutne podstawy

Async i Await : maszyna stanów pamięta zakończenie

                    

                        private void Button_Click_1(object sender, RoutedEventArgs e)
                        {
                            int number1 = int.Parse(txtNumber1.Text); 
                            int number2 = int.Parse(txtNumber2.Text); 
                            int result = 0;
                            Thread.Sleep(4000);
                            result = number1 + number2;
                        
                            MessageBox.Show(result.ToString());
                        }
                    
                

blog
Copyright © Cezary Walenciuk

Async i Await : maszyna stanów pamięta zakończenie

                    

                        private async Task Calculate(int number1, int number2)
                        {
                            return await Task.Run(() =>
                            {
                                Thread.Sleep(4000);
                                return number1 + number2;
                            });
                        }
                    
                

blog
Copyright © Cezary Walenciuk

Async i Await : maszyna stanów pamięta zakończenie

                    

                        private  async void Button_Click_1(object sender, RoutedEventArgs e)
                        {
                            int number1 = int.Parse(txtNumber1.Text); 
                            int number2 = int.Parse(txtNumber2.Text); 
                            int result = await Calculate(number1, number2);

                            MessageBox.Show(result.ToString(CultureInfo.InvariantCulture));

                        }
                    
                

blog
Copyright © Cezary Walenciuk
blog
Copyright © Cezary Walenciuk
blog
Copyright © Cezary Walenciuk
Synchronizacja ze wszystkim

Synchronizacja, jest zależna od aplikacji

3 warstwy synchronizacji

Domyślny TaskScheduler jest oparty na puli wątków, czyli o klasę "ThreadPool" którą omówiliśmy wcześniej.
Jest to potrzebne do zrozumienia ConfigureAwait(false)
SychronizactionContext
blog
Copyright © Cezary Walenciuk
Każde określenie "await" przechwytuje pewien kontekst synchronizacyjny i później używa on go aby powrócić do swojego stanu, który go wywołał.

SynchronizationContext Post

                    

                        public class SychronizationContext
                        {
                           public virtual void Post(SendOrPostCallback d, Object state)
                           {
                              ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
                           }
                        
                        }
                    
                

SynchronizationContext Send

                    

                        public class SychronizationContext
                        {
                           public virtual void Send(SendOrPostCallback d, Object state)
                           {
                              d(state);
                           }
                        
                        }
                    
                

Windows Forms SynchronizationContext Post

                    

                        public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable 
                        {
                           public override void Post(SendOrPostCallback d, Object state) {

                               if (controlToSendTo != null) {
                                   controlToSendTo.BeginInvoke(d, new object[] { state });
                               }
                           }
                        
                           public override SynchronizationContext CreateCopy() {
                               return new WindowsFormsSynchronizationContext(controlToSendTo, DestinationThread);
                           }
                        
                        }
                    
                

Windows Forms SynchronizationContext Send

                    

                        public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable 
                        {
                            public override void Send(SendOrPostCallback d, Object state) {
                                Thread destinationThread = DestinationThread;
                                if (destinationThread == null || !destinationThread.IsAlive) {
                                    throw new InvalidAsynchronousStateException(SR.GetString(SR.ThreadNoLongerValid));
                                }
                            
                                if (controlToSendTo != null) {
                                    controlToSendTo.Invoke(d, new object[] { state });
                                }
                            }

                        }
                    
                

WPF SynchronizationContext Post

                    

                        public sealed class DispatcherSynchronizationContext : SynchronizationContext
                        {
                        
                           /// <summary>
                           ///     Asynchronously invoke the callback in the SynchronizationContext.
                           /// </summary>
                           public override void Post(SendOrPostCallback d, Object state)
                           {
                               // Call BeginInvoke with the cached priority.  Note that BeginInvoke
                               // preserves the behavior of passing exceptions to
                               // Dispatcher.UnhandledException unlike InvokeAsync.  This is
                               // desireable because there is no way to await the call to Post, so
                               // exceptions are hard to observe.
                               _dispatcher.BeginInvoke(_priority, d, state);
                           }
                        
                        }
                    
                

WPF SynchronizationContext Send

                    

                        public sealed class DispatcherSynchronizationContext : SynchronizationContext
                        {
                            /// <summary>
                            ///     Synchronously invoke the callback in the SynchronizationContext.
                            /// </summary>
                            public override void Send(SendOrPostCallback d, Object state)
                            {
                                // Call the Invoke overload that preserves the behavior of passing
                                // exceptions to Dispatcher.UnhandledException.  
                                if(BaseCompatibilityPreferences.GetInlineDispatcherSynchronizationContextSend() 
                                && _dispatcher.CheckAccess())
                                {
                                    // Same-thread, use send priority to avoid any reentrancy.
                                    _dispatcher.Invoke(DispatcherPriority.Send, d, state);
                                }
                                else
                                {
                                    // Cross-thread, use the cached priority.
                                    _dispatcher.Invoke(_priority, d, state);
                                }
                            }
                        }
                    
                

Jaki problem "SynchronizationContext" chce rozwiązać.
W końcu SynchronizationContext reprezentuje obecnie środowisko, na którym nasz kod się uruchamia

W asynchronicznym programie, gdy wysyłamy delegate/zadanie do innego wątku musimy złapać obecne środowisko, na którym się znajdujemy umieścić go do obiektu "SynchronizationContext"...i wsadzić jego do obiektu Task.
Mamy więc kontekst środowiska i wrzucamy go do innego wątku. To jak inne wątki będą trzymać swoje kontekst jest zależne od aplikacji.

Co ciekawe są frameworki, które tego kontekstu nie wysyłają. ASP.NET CORE nie ma "SynchronizationContext"
A co z ASP.NET CORE?
SynchronizationContext został usunięty z ASP.NET CORE.
Istnieją moim zdaniem dwa powody. Wydajność i prostota aplikacji.
Kiedyś w ASP.NET istniał chwytacz (handler), który łapał każde zapytanie do twojej strony internetowej. On w sobie tworzył kolejkę, ponieważ każde zapytanie musiało czekać na inne wcześniejsze zapytania. Nic nie jest natychmiastowe.

Zapytania do serwera stały więc swojej kolejce. Aby zapytania się nie zgubiły i nie pomieszały musiały do nich utworzyć się KONTEKST.

Gdy dane zapytanie było gotowe do uruchomienia przez serwer wtedy wątek był pobierany z puli wątków i wchodził on w KONTEKST SWOJEGO ZAPYTANIA. Później on zaczął obsługę swojego zapytania HTTP do serwera i na przykład wyświetla Ci stronę ".aspx".
Oczywiście trzeba było wykonać dużo operacji, aby te ponowne wejście w kontekst zapytania trzymało się kupy.

Obiekt "HttpContext.Current" musiał zawierać w sobie informację o obecnym wątku, jak i danej kulturze, w której został uruchomiony.
Teraz w ASP.NET CORE, gdy asynchroniczny chwytacz wznawia swoje zadanie -- wątek jest brany z puli wątków i wykonuje swoje zadanie...bez żadnego wchodzenia ponownie w kontekst swojej kreacji.

ASP.NET CORE nie ma kontekstu, gdyż usunęli oni potrzebę tworzenia go poprzez pozbycie się tej kolejki zapytań na początku całego tego procesu.
Potem programiści stwierdzili, że w sumie ten kontekst nigdzie nie jest potrzebny

Kiedyś aplikacja ASP.NET MVC musiała blokować operację dla filtrów i pod akcji kontrolerów.
W ASP.NET CORE cały potok jest asynchroniczny.

Dotyczy to też operacji "async i await" . Te operacje asynchroniczne też nie mają kontekstu do powrotu.

Nie dziw się więc, gdy ASP.NET CORE nawołuje Cię do korzystania z asynchronicznych metod nawet po stronie stron ".cshtml" Razor.
Nie musisz też korzystać z "ConfigureAwait(false)", chociaż pamiętaj o nim, gdy piszesz bibliotekę, która może być używane nie tylko przez ASP.NET CORE.

Ten kod zadziała róźnie w zależności od tego czy jest to ASP.NET CORE czy stary

                    

                        private HttpClient _client = new HttpClient();

                        async Task<List<string>> GetBothAsync(string url1, string url2)
                        {
                            var result = new List<string>();
                            var task1 = GetOneAsync(result, url1);
                            var task2 = GetOneAsync(result, url2);
                            await Task.WhenAll(task1, task2);
                            return result;
                        }
                        
                        async Task GetOneAsync(List<string> result, string url)
                        {
                            var data = await _client.GetStringAsync(url);
                            result.Add(data);
                        }
                    
                

W ASP.NET HttpClient wykona po kolei operacje asynchroniczne - w jednym wątku - jak w kolejce. każda operacja HttpClient będzie czekała na wcześniejszą przez "Request Context".

W ASP.NET CORE obie operacje pobrania zawartości strony WWW przez HttpClient uruchomią się równocześnie co spowoduje, że twoja lista napisów może wyglądać zupełnie inaczej.

3 warstwy synchronizacji dla ASP.NET CORE

Wracając do "SynchronizationContext". Określa on lokalizację "gdzie" twój kod ma wrócić, gdy wykona swoją operację.
Kiedy przechwytywanie obecnego "SynchronizationContex" jest potrzebne
WPF powiedzmy, że chcesz wysłać rezultat swojego działania do pola testowego i jego właściwości Text.

Jest jednak tutaj problem. Tylko wątek UI w WPF ma prawo do zmiany tej właściwości w tej kontrolce. Jeżeli spróbujesz coś takiego zrobić to dostaniesz wyjątek "InvalidOperationException".
Podobnie jest w Windows Forms. Kontrolki WPF i Windows Form mogą być modyfikowane tylko przez wątek, które je utworzył. Jest nim główny wątek interfejsu użytkownika.
Istnieje jeszcze inny problem w WPF i jest nim SecurityContext, który pozwala na wykonanie pewne operacji w pewnym wątku, ale nie w innym. Wszystko to możesz jednak naprawić takim kodem.

Istnieje on w ExecutionContext

Możesz to wszystko naprawić taki kodem

                    

                        public static void DoWork()
                        {
                            //W wątku UI
                            var sc = SynchronizationContext.Current;
                        
                            ThreadPool.QueueUserWorkItem(delegate
                            {
                               // zrób coś w ThreadPool w innym wątku
                                sc.Post(delegate
                                {
                        
                                     // zrób prace na wątku (UI)
                        
                                }, null);
                            });
                        }
                    
                

Wygląda to skomplikowanie. Na szczęście "async i await" operują na SychronizationContext pod stołem bez naszej wiedzy.

SychronizationContext pod stołem bez naszej wiedzy.

                    
                        await LetsDoSomethingAsync();

                        ContinueWithRestOfThisCode();
                    
                

Ten kod tak naprawdę wygląda tak :

                    
                        var task = LetsDoSomethingAsync();
                        var currentSyncContext = SynchronizationContext.Current;
                        
                        task.ContinueWith(delegate
                        {
                            if (currentSyncContext == null) ContinueWithRestOfThisCode();
                            else currentSyncContext.Post(delegate { ContinueWithRestOfThisCode(); }, null);
                        
                        }, TaskScheduler.Current);
                    
                

Ten kod tak naprawdę wygląda tak :

                    
                        var task = LetsDoSomethingAsync();
                        var currentSyncContext = SynchronizationContext.Current;
                        
                        task.ContinueWith(delegate
                        {
                            if (currentSyncContext == null) ContinueWithRestOfThisCode();
                            else currentSyncContext.Post(delegate { ContinueWithRestOfThisCode(); }, null);
                        
                        }, TaskScheduler.Current);
                    
                

Programowanie w async i await jest łatwiejsze

                    

                        private async void Button_Click(object sender, RoutedEventArgs e)
                        {
                            var i = await LetsDoSomethingAsync();
                        
                            ContinueWithRestOfThisCode(i);
                        }
                        
                        private Task LetsDoSomethingAsync()
                        {
                            Task.Delay(1000);
                            return Task.FromResult(1);
                        }
                        
                        private void ContinueWithRestOfThisCode(int i)
                        {
                            ResultTextBlock.Text = i.ToString();
                        }
                    
                

Niebezpieczeństwo WPF i Windows Forms "SychronizactionContext" i ".Result"

Result : Co się tutaj stanie

                    
                        public partial class MainWindow : Window
                        {
                            private HttpClient HttpClient = new HttpClient();
                            private void Button_Click(object sender, RoutedEventArgs e)
                            {
                                var result = HttpClient.GetStringAsync("https://postman-echo.com/delay/10").Result;
                                ResultTextBlock.Text = result;
                            }
                        
                            public MainWindow()
                            {
                                InitializeComponent();
                            }
                        }
                    
                

Result :

                    
                        public partial class MainWindow : Window
                        {
                            private HttpClient HttpClient = new HttpClient();
                            private async void Button_Click(object sender, RoutedEventArgs e)
                            {
                                var result = await HttpClient.GetStringAsync("https://postman-echo.com/delay/10");
                                ResultTextBlock.Text = result;
                            }
                        
                            public MainWindow()
                            {
                                InitializeComponent();
                            }
                        }
                    
                

Jak zrobić deadlock

Result :

                    

                        private readonly static string _url = "https://postman-echo.com/delay/10";

                        private void Button_Click(object sender, RoutedEventArgs e)
                        {
                            ResultTextBlock.Text = "";
                            ResultTextBlock.Text += "Przed MyGetStringAsync()";
                            var result = MyGetStringAsync().Result;
                            ResultTextBlock.Text += "Napisze coś w międzyczasie";
                            ResultTextBlock.Text = result;
                            ResultTextBlock.Text += "Po MyGetStringAsync()";
                        }
                        
                        public async Task<string> MyGetStringAsync()
                        {
                            using (HttpClient httpClient = new HttpClient())
                            {
                                return await httpClient.GetStringAsync(_url);
                            }
                        }
                    
                

Teraz wymieszałem kod synchroniczny z asynchronicznym. Jak jednak zrobiłem tutaj deadlock. Jak to możliwe, że aplikacja się zawiesza na wieczność.

Otóż moja metoda MyGetStringAsync czeka na wątek główny UI, ale z drugiej strony ten sam wątek główny czeka na rezultat mojej metody.
Czyli jakiś cudem stworzyłem sytuacje, w której wątek główny czeka na mój rezultat, ale ten nigdy nie nastąpi, ponieważ moja metoda czeka, aż wątek główny przestanie czekać na mój rezultat.

Kto by pomyślał, że wywoływanie .Result czy Wait może rozwalić cały ten SynchronizationContext. Spróbujmy jednak wyjaśnić co dokładnie się tutaj stało.
Wiemy, że główny wątek UI czeka na moją metodę asynchroniczną. Na co jednak dokładnie moja metoda asynchroniczna czeka?

SynchronizationContext jest w tym przykładzie używany i to jest problem. Próbuje do niego wróci, ale nie mogę, bo on jest zablokowany.

SynchronizationContext wróciłby w tym wypadku do kodu w metodzie "Button_Click" tylko mi to nie jest potrzebne, bo korzystam z właściwości Result.

Pamiętasz ten przykład przed chwilą :

                    
                        await LetsDoSomethingAsync();

                        ContinueWithRestOfThisCode();
                    
                

O co chodzi z ConfigureAwait(false)

ConfigureAwait(false);

                    
                        private void Button_Click(object sender, RoutedEventArgs e)
                        {
                            ResultTextBlock.Text = "";
                            ResultTextBlock.Text += "Przed MyGetStringAsync()";
                            var result = MyGetStringAsync().Result;
                            ResultTextBlock.Text += "Napisze coś w międzyczasie";
                            ResultTextBlock.Text = result;
                            ResultTextBlock.Text += "Po MyGetStringAsync()";
                        }
                        
                        public async Task<string> MyGetStringAsync()
                        {
                            using (HttpClient httpClient = new HttpClient())
                            {
                                return await httpClient.GetStringAsync(_url)
                                    .ConfigureAwait(false);
                            }
                        }
                    
                

Dodaliśmy ConfigureAwait(false). Mówi on aplikacji, że nie będzie kontynuowana żadna operacja z nim związana.

Nasza maszyna stanów ma zignorować SychronzationContext, który został pobrany. Nie chcemy wracać do tego kontekstu.
Trzeba pamiętać, że to ustawienie trzeba ustawić z każdym wywołaniem "async i await".

Przykładowo moja metoda "MyGetStrinAsync" nie interesuje się kontekstem wykonawczy "button_Click" i w tym wypadku ma to sens.

ConfigureAwait(false); w starym ASP.NET

                    
                        public class ExampleController : Controller
                        {
                            public string Get()
                            {
                                MyProxy proxy = new MyProxy();
                                var r = proxy.MyGetStringAsync(
                                    new Uri("https://postman-echo.com/get")).Result;
                        
                                return r;
                            }
                        }
                        
                        public class MyProxy
                        {
                            public async Task<string> MyGetStringAsync(Uri uri)
                            {
                                using (var client = new HttpClient())
                                {
                                    var value = await client.GetStringAsync(uri)
                                        .ConfigureAwait(false);
                        
                                    return value;
                                }
                            }
                        }
                    
                

Kiedy używać ConfigureAwait(false)

Kiedy używać ConfigureAwait(false)?

                    
                        private async void Button_Click(object sender, RoutedEventArgs e)
                        {
                            string result = "";
                            using (HttpClient httpClient = new HttpClient())
                            {
                                result = await httpClient.GetStringAsync(_url)
                                    .ConfigureAwait(false);
                            }
                        
                            ResultTextBlock.Text = result;
                            //BUM
                        }
                        
                         private readonly static string _url = "https://postman-echo.com/delay/2";
                    
                

Kiedy używać ConfigureAwait(false)?

                    
                        private async void Button_Click(object sender, RoutedEventArgs e)
                        {
                            string result = "";
                            using (HttpClient httpClient = new HttpClient())
                            {
                                result = await httpClient.GetStringAsync(_url)
                                    .ConfigureAwait(false);
                            }
                        
                            ResultTextBlock.Text = result;
                            //BUM
                        }
                        
                         private readonly static string _url = "https://postman-echo.com/delay/2";
                    
                

SynchronizationContext :
o czym uczy
Zapomnij o task.ContinueWith

Nie musimy pisać takiego kodu

                    
                        var task = LetsDoSomethingAsync();

                        var currentSyncContext = SynchronizationContext.Current;
                        
                        task.ContinueWith(delegate
                        {
                            if (currentSyncContext == null) ContinueWithRestOfThisCode();
                            else currentSyncContext.Post(delegate { ContinueWithRestOfThisCode(); }, null);
                        
                        }, TaskScheduler.Current);
                    
                

Nie musimy pisać takiego kodu

                    
                        string result = "";
                        using (HttpClient httpClient = new HttpClient())
                        {
                            result = await httpClient.GetStringAsync(_url)
                                .ContinueWith(pageHtml => 
                                { 
                                  //Twoj kod UI
                                  return pageHtml.Result; 
                                }
                                ,TaskScheduler.FromCurrentSynchronizationContext());
                        }
                    
                

Nie musimy pisać takiego kodu

                    

                        SynchronizationContext synchronizationContext = 
                        SynchronizationContext.Current;

                        string result = "";
                        using (HttpClient httpClient = new HttpClient())
                        {
                            result = await httpClient.GetStringAsync(_url)
                                .ContinueWith(pageHtml =>
                                {
                                    synchronizationContext.Post(__ => {
                        
                                        //Twoj kod UI
                        
                                    }, null);
                                   
                                    return pageHtml.Result;
                                });
                        }
                    
                

Czasem może chciałbyś taki kod napisać

Nie musimy pisać takie kodu

                    

                        public void Do(Action job, Action onDone)
                        {
                           SynchronizationContext sc = SynchronizationContext.Current;
                        
                           ThreadPool.QueueUserWorkItem(_ =>
                           {
                               try
                               {
                                   job();
                               }
                               finally
                               {
                                   sc.Post(__ => onDone(), null);
                               }
                           });
                        }
                    
                

Możesz utworzyć swój kontekst synchronizujący

Możesz utworzyć swój kontekst synchronizujący

                    
                        Task.Run(async delegate
                        {
                            SynchronizationContext.SetSynchronizationContext
                            (new MySynchronizationContext());

                            await MyGetStringAsync();
                        });
                    
                

Możesz utworzyć swój kontekst synchronizujący

                    
                        public class MySynchronizationContext : SynchronizationContext
                        {
                            public override void Post(SendOrPostCallback d, object state)
                            {
                                Console.WriteLine("Sychronizacja się wykonuje");
                                base.Post(d, state);
                            }
                        }
                    
                

TaskScheduler

Co ma TaskScheduler

Co ma TaskScheduler

Co ma TaskScheduler

W ConcurrentExclusiveSchedulerPair masz do dyspozycji dwa TaskSchedulery

                    
                        var cesp = 
                        new ConcurrentExclusiveSchedulerPair();
                    
                

Co ma TaskScheduler

W ConcurrentExclusiveSchedulerPair masz do dyspozycji dwa TaskSchedulery

                    
                        var cesp = new ConcurrentExclusiveSchedulerPair();

                        Task.Factory.StartNew(() =>
                        {
                        
                            Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);
                        
                        }, default, TaskCreationOptions.None,
                        cesp.ExclusiveScheduler).Wait();
                    
                

W ConcurrentExclusiveSchedulerPair masz do dyspozycji dwa TaskSchedulery

                    
                        var cesp = new ConcurrentExclusiveSchedulerPair();

                        var con = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 8)
                            .ConcurrentScheduler;
                        var exc = new ConcurrentExclusiveSchedulerPair(con).ExclusiveScheduler;
                    
                

Praktyczne użycie "FormCurrentSynchronizationContext"

                    
                        string result = "";
                        using (HttpClient httpClient = new HttpClient())
                        {
                            result = await httpClient.GetStringAsync(_url)
                                .ContinueWith(pageHtml => 
                                { 
                                  //Twoj kod UI
                                  return pageHtml.Result; 
                                }
                                ,TaskScheduler.FromCurrentSynchronizationContext());
                        }
                    
                

ExecutionContext

ExecutionContext jest pojemnikiem na te inne konteksty

Uniknięcie śledzenia przez ExecutionContext

                    
                        ThreadPool.UnsafeQueueUserWorkItem
                    
                

Async i Await Task API

Agenda

Awaitables

Awaitables

                    
                        TaskAwaiter awaiter = task.GetAwaiter();
                    
                

Awaitables

                    
                        static async Task Main()
                        {
                            int a = await RandomNumberAsync();
                        }
                        
                        private static Task RandomNumberAsync()
                        {
                            return Task.FromResult(4);
                        }
                    
                

Awaitables IsCompleted

                    

                        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);
                        }
                    
                

blog
Copyright © Cezary Walenciuk

Awaitables OnCompleted

                    
                        Task<int> awaitableType = RandomNumberAsync();
                            var awaiter = awaitableType.GetAwaiter();
                            
                            awaiter.OnCompleted(
                                () => { Console.WriteLine("Koniec"); }
                            );
                    
                

Pytania, które warto zadać

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

Obiekt zwracany przez metodę GetAwaiter() musi mieć

Awaitables

                    
                        TaskAwaiter awaiter = task.GetAwaiter();
                    
                

MyAwaitable

                    
                        public class MyAwaitable
                        {
                            public MyAwaiter GetAwaiter() => new MyAwaiter();
                        }
                    
                

MyAwaitable

                    

                        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";
                            }
                        }
                    
                

MyAwaitable

                    
                        static async Task Main()
                        {
                            var s = await new MyAwaitable();
                            Console.WriteLine(s);
                        }
                    
                

W tym wypadku

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

MyAwaitable

                    
                        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";
                            }
                        }
                    
                

Właściwość IsCompleted jest sprawdzana przez maszynę stanów tylko raz.

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

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

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

IsCompleted jest zawsze 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";
                            }
                        }
                    
                

W tym wypadku

Pytanie 3 : Co dokładnie symbolizuje delegata Action "continuation".
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()"
blog
Copyright © Cezary Walenciuk
Czy zauważyłeś, że metoda GetResult() w tych przykładach zwraca "string", ale zaraz przecież rezultatem akcji asynchronicznej mogą być nie tylko napisy.

IsCompleted jest zawsze FALSE

                    
                        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

                    

                        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);
                        }
                    
                

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

                    

                        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();
                            }
                        }
                    
                

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

                    
                        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.

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

                    

                        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();
                        }
                    
                

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

                    
                        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".

Lepsza klasa "Awaitable" otwiera oczy na mechanizmy 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.

W tym wypadku

Na szczęście w swojej karierze nie będziesz musiał pisać takiego kodu.
Sensowne użycie awaitable z TaskAwaiter

CultureAwaiter

                    

                        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();
                            }
                        }
                    
                

ControlAwaiter

                    

                        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()".

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

                    

                        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();
                            }
                        }
                    
                

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

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

TaskCompletionSource
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ład uruchamiania procesu

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

Przykład uruchamiania procesu

                    

                        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.

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

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 jak robię await Task
https://www.markopapic.com/csharp-under-the-hood-async-await/

Maszyna stanów async i await

                    
                        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);
                        }
                    
                

Maszyna stanów async i await

                    
                        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;
                        }
                    
                

Maszyna stanów async i await

                    
                        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);
                          }
                        }
                    
                

blog
Copyright © Cezary Walenciuk
blog
Copyright © Cezary Walenciuk
Task.Yield
Czasami chciałbyś mieć pewność, że dalsze operacje po wykonaniu danego zadania na pewno też będą wykonywane asychroniczne

Task.Yield

                    

                        public struct YieldAwaiter : INotifyCompletion
                        {
                            public void OnCompleted(Action continuation)
                            {
                                ThreadPool.QueueUserWorkItem(new WaitCallback
                                    (
                                        (a) => { continuation(); }
                                    ));
                            }
                        
                            public bool IsCompleted
                            {
                                get
                                {
                                    return false;
                                }
                            }
                        
                            public void GetResult()
                            {
                        
                            }
                        }
                    
                

Task.Yield zwraca gotowego "Awaiter-a" do takiego celu

Task.Yield

                    
                        [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);
                                }
                            }
                        }
                    
                

Task.Yield

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

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.
Inne mechanizmy wątku UI, które mają niższy priorytety (jak reakcje na użytkownika) w wyniku działania "Task.Yield" będą blokowane.

Task.Yield to kiepski pomysł

                    
                        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();
                            }
                        }
                    
                

Task.Yield to kiepski pomysł

                    
                        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ętle zdarzeń. Ta wiadomość będzie wymuszona i co wie co zablokuje po drodze i mimo wszystko wykona się na tym samym wątku.

Gdybyś chciał osiagnąć taki cel to byś napisał taki kod

                    
                        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++;
                            }
                        }
                    
                

Gdybyś chciał osiagnąć taki cel to byś napisał taki kod

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

Task.Yield nie ma prawie zastosowania. Wyjątkiem jest pewien bug w ASP.NET i przypadki testów jednostkowych, które muszą być asychroniczne