W poprzednim poście posłużyłem się następującym kodem, mającym wczytać plik ze zrzutem pamięci i następnie załadować odpowiednią wersję pliku mscordacwks.dll:
use target = DataTarget.LoadCrashDump(@"..\..\..\..\data\ConsoleDump.exe.dmp")
target.SymbolLocator.SymbolPath <- @"SRV*http://msdl.microsoft.com/download/symbols"
target.SymbolLocator.SymbolCache <- @"c:\symbols"
for version in target.ClrVersions do
let dacInfo = version.DacInfo
let runtime = version.CreateRuntime()
Niestety, jak już wspomniałem, ten kod umieszczony w aplikacji WPF powoduje jej zawieszenie - na wywołaniu CreateRuntime() interfejs przestaje odpowiadać i możemy czekać tak w nieskończoność. Wygląda jak deadlock, więc popatrzmy co w tym czasie robi główny wątek aplikacji:
Każdy kto trochę umoczył ręce w async/await/TPL w kontekście np. WPF pewnie dopatrzy się o co chodzi.
Więc o co chodzi? Ciąg wydarzeń jest taki: nasze wywołanie ClrInfo.CreateRuntime() ląduje w obiekcie DefaultSymbolLocator, który nie znajdując pliku lokalnie ostatecznie wywołuje metodę GetPhysicalFileFromServer:
var req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(fullUri);
req.UserAgent = "Microsoft-Symbol-Server/6.13.0009.1140";
req.Timeout = Timeout;
var response = req.GetResponse();
using (var fromStream = response.GetResponseStream())
{
// ...
CopyStreamToFile(fromStream, fullUri, fullDestPath, response.ContentLength);
return fullDestPath;
}
Metoda CopyStreamToFile jest prosta:
protected override void CopyStreamToFile(Stream stream, string fullSrcPath, string fullDestPath, long size)
{
CopyStreamToFileAsync(stream, fullSrcPath, fullDestPath, size).Wait();
}
Gdzie:
async Task CopyStreamToFileAsync(Stream input, string fullSrcPath, string fullDestPath, long size)
I tu leży pies pogrzebany - pozwoliłem go sobie wyróżnić. Wykonujemy Wait na async Tasku! A to oznacza w przypadku WPF, że nasz wątek główny (ten od interfejsu) czeka na DispatcherSynchronizationContext, na którym próbuje się również wywołać kontynuacja taska – bo domyślnie wykorzystuje on właśnie również DispatcherSynchronizationContext. I mamy deadlock. Jedno nie ruszy bez drugiego.
To niezbyt intuicyjna część używania asynców, na którą z dużym prawdopodobieństwem natknie się każdy, kto zaczyna zabawę z ich użyciem. Jednak nie spodziewałbym się, że natkniemy się na ten problem w bibliotece takiej jak ClrMd.
After answering many async-related questions on the MSDN forums, Stack Overflow and e-mail, I can say this is by far the most-asked question by async newcomers once they learn the basics: “Why does my partially async code deadlock?” [5]
Co do de facto oznacza? Błąd, czy też niedopatrzenie, w bibliotece ClrMD. Autor biblioteki nigdy nie powinien wołać Wait na asyncu właśnie z tego powodu, że zachowanie tej kombinacji zależy od konkretnego ContextSynchronization w kontekście aplikacji. Ten kod na przykład zadziała w aplikacji konsolowej, bo tam ContextSynchronization.Current jest nullem i wtedy kontynuacja używa puli wątków poprzez ThreadPool.QueueUserWorkItem i mamy prostą sytuację – kontynuacja wykonuje się na innym wątku i nie ma nic wspólnego z wątkiem wywołującym.
Jak to naprawić? Pierwszy i najprostszy sposób to uwzględnienie tego zachowania po stronie aplikacji. Jeśli wiemy bowiem, że biblioteka brzydko się zachowuje, możemy świadomie zepchnąć jej działanie z wątku głównego do wątku działającego w tle. Posłużyć się w tym celu możemy np. F# async workflow, czyniąc metodę Load asynchronicznym workflow:
member this.Load() = async {
use target = DataTarget.LoadCrashDump(@"..\..\..\..\data\SampleConsoleApplication.exe.dmp")
target.SymbolLocator.SymbolPath <- @"SRV*http://msdl.microsoft.com/download/symbols"
target.SymbolLocator.SymbolCache <- @"c:\symbols"
for version in target.ClrVersions do
let dacInfo = version.DacInfo
let runtime = version.CreateRuntime()
// ...
}
I z wątku głównego (interfejsu) czekamy co prawda synchronicznie:
dump.Load() |> Async.RunSynchronously
…ale ponieważ użyliśmy async workflow, cała robota odbywa się na ThreadPool i jest niezależna od DispatcherSynchronizationContext. W efekcie zakleszczenie znika i nasz program zaczyna działać poprawnie!
Issue #2 został rozwiązany.
Jednak pozostaje pewien niedosyt, bowiem obeszliśmy błąd, pozostawiając go samemu sobie. A przecież ClrMD to pełny Open Source, dlatego w następnym wpisie przystąpię do poprawiania tejże biblioteki.
Metariały dodatkowe – warto poczytać – ale zajmie to kilka kaw:
1. The Perfect Recipe to Shoot Yourself in The Foot - Ending up with a Deadlock Using the C# 5.0 Asynchronous Language Features
2. Await, SynchronizationContext, and Console Apps
3. Don't Block on Async Code
4. Parallel Computing - It's All About the SynchronizationContext
5. Async/Await - Best Practices in Asynchronous Programming
6. What does SynchronizationContext do?
7. ExecutionContext vs SynchronizationContext