Skip to content

Instantly share code, notes, and snippets.

@marcin-chwedczuk
Last active December 30, 2018 10:35
Show Gist options
  • Save marcin-chwedczuk/ee9f0efa18fb3f395724c8eab75dd7bc to your computer and use it in GitHub Desktop.
Save marcin-chwedczuk/ee9f0efa18fb3f395724c8eab75dd7bc to your computer and use it in GitHub Desktop.
O lokalności `ConfigureAwait`

O lokalności ConfigureAwait

Chciałbym tu naświetlić pewną kwestię związaną z użyciem ConfigureAwait którą wiele osób uczących się programowania asynchronicznego w C# przeocza.

Zacznijmy od prostego przykładu aplikacji WinForms składającej się z jednego przycisku:

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
   }
   private async void button1_Click(object sender, EventArgs e)
   {
       button1.Text = "Before";
       await Wait(1);
       button1.Text = "After";
   }
   private static async Task Wait(int seconds)
   {
       await Task.Delay(seconds * 1000);   
   }
}

Zrozumienie działania tej aplikacji nikomu nie powinno przysporzyć kłopotów. Użytkownik naciska przycisk, my zmieniamy tekst przycisku na Before by po sekundzie zmienić go ponownie na After. Zero magii.

Dodatkowo przypomnę że w WinForms podobnie jak w WPF czy w klasyncznym ASP.NET (a więc nie Core), zadania asynchroniczne wystartowane z głównego wątku aplikacji (UI thread) czy z wątku obsługującego dane żądanie HTTP (w ASP.NET) będą kontynuowane przy użyciu tego samego wątku który jest wystartował. Oczywiście może to prowadzić do zakleszczeń (ang. deadlock), ale o tym sporo już napisano a ja nie lubę się powtarzać.

Jednym ze sposobów który stosuje się w celu minimalizacji prawdopodobieństwa wystąpienia zakleszczenia jest użycie ConfigureAwait(false) w kodzie bibliotek. W dużym uproszczeniu ConfigureAwait(false) mówi tyle że kontynuacja metody asynchronicznej może zostać wykonana na dowolnym wątku.

Jak to wygląda w praktyce, jeżeli w naszej przykładowej aplikacji napiszemy:

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
   }
   private async void button1_Click(object sender, EventArgs e)
   {
       button1.Text = "Before";
       await Wait(1).ConfigureAwait(false); // <- Code change here!
       button1.Text = "After";
   }
   private static async Task Wait(int seconds)
   {
       await Task.Delay(seconds * 1000);   
   }
}

To tym razem wciśnięcie przycisku będzie skutkować wyjątkiem. Dlaczego? Część metody asynchronicznej występująca po await Wait(1).ConfigureAwait(false); zostanie z dużym prawdopodobieństwem wykonana na jednym z wątków pochodzących ze standardowej puli wątków (ThreadPool). Wątek ten będzie próbował zmienić właściwość komponentu UI, który może być modyfikowany bez synchronizacji (bez użycia metody Invoke) tylko i wyłącznie z głównego wątku aplikacji (ang. UI thread). Zostanie to wykryte przez nasz przycisk, który następnie zgłosi błąd. Kaboom!

Powoli zbliżamy się do sedna sprawy. Co się stanie jeżeli przeniesiemy wywołanie ConfigureAwait(false) do metody Wait?

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
   }
   private async void button1_Click(object sender, EventArgs e)
   {
       button1.Text = "Before";
       await Wait(1); // <- Code change here!
       button1.Text = "After";
   }
   private static async Task Wait(int seconds)
   {
       // More calls to proof the point...
       await Task.Delay(seconds * 333).ConfigureAwait(false); // <- Code change here!
       await Task.Delay(seconds * 333).ConfigureAwait(false);
       await Task.Delay(seconds * 334).ConfigureAwait(false);
   }
}

Absolutnie nic złego. Nasza aplikacja znowu będzie działać bez żadnego problemu. Dlaczego? Ponieważ ConfigureAwait może zmienić zachowanie tylko tej części asynchronicznego kodu który występuje bezpośrednio po jego wywołaniu i tylko w obrębie pojedynczej metody. W szczególności metody "rodzice" które wywołały daną metodę asynchroniczną nigdy nie odczują skutków ConfigureAwait. Metody "dzieci" wywoływane przez daną metodę zawierającą ConfigureAwait, odczuwają skutki ConfigureAwait pośrednio. To jest, ponieważ kod rodzica występujący po ConfigureAwait jest kontynuowany na wątku ze standardowej puli wątków, to również kod metody "dziecka" wywołany przez to kontynuację będzie wykonywany na jednym z wątków z puli (porównajcie to z sytuacją gdy z kodu usunięto by wszystkie wywołania ConfigureAwait(false)).

I właśnie dlatego możemy w kodzie bibliotek solić ConfigureAwait(false) na prawo i lewo, bez przejmowania się konsekwencjami.

KONIEC

@marcin-chwedczuk
Copy link
Author

Dzięki @shukibruck! Literówkę poprawiłem, film na pewno obejrzę 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment