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
Dzięki @shukibruck! Literówkę poprawiłem, film na pewno obejrzę 👍