Blog Kokosa

.NET i okolice, wydajność, architektura i wszystko inne

NAVIGATION - SEARCH

MemoryVisualizer - ClrMd i deadlock

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:

Issue4

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

blog comments powered by Disqus