Ten kod zawiesza się w trybie wydania, ale działa dobrze w trybie debugowania


110

Natknąłem się na to i chcę poznać przyczynę tego zachowania w trybie debugowania i wydania.

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

25
jaka dokładnie jest różnica w zachowaniu?
Mong Zhu

4
Gdyby to była java, założyłbym, że kompilator nie widzi aktualizacji zmiennej „kompiluj”. Dodanie „volatile” do deklaracji zmiennej rozwiązałoby ten problem (i uczyniłoby z niego pole statyczne).
Sebastian


4
Uwaga: to jest powód, dla którego tworzenie wielowątkowe ma takie rzeczy, jak muteksy i operacje atomowe. Kiedy zaczynasz korzystać z wielowątkowości, musisz wziąć pod uwagę niezwykły zestaw dodatkowych problemów z pamięcią, które wcześniej nie były oczywiste. Narzędzia do synchronizacji wątków, takie jak muteksy, rozwiązałyby ten problem.
Cort Ammon

5
@DavidSchwartz: Z pewnością jest to dozwolone . I to jest dozwolony dla kompilatora, wykonywania i CPU do uczynienia wynikiem tego kodu być inna niż to, co można się spodziewać. W szczególności C # nie może uczynić dostępu bool nieatomowym , ale dozwolone jest przenoszenie odczytów nieulotnych wstecz w czasie. Natomiast dublety nie mają takiego ograniczenia atomowości; dozwolone jest zerwanie podwójnego odczytu i zapisu w dwóch różnych wątkach bez synchronizacji.
Eric Lippert

Odpowiedzi:


149

Wydaje mi się, że optymalizator daje się nabrać na brak słowa kluczowego „volatile” w isCompletezmiennej.

Oczywiście nie możesz tego dodać, ponieważ jest to zmienna lokalna. I oczywiście, ponieważ jest to zmienna lokalna, w ogóle nie powinna być potrzebna, ponieważ lokalne pliki są trzymane na stosie i naturalnie zawsze są „świeże”.

Jednak po kompilacji nie jest już zmienną lokalną . Ponieważ dostęp do niego uzyskuje się przez anonimowego delegata, kod jest dzielony i tłumaczony na klasę pomocniczą i pole składowe, na przykład:

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

Mogę sobie teraz wyobrazić, że kompilator / optymalizator JIT w środowisku wielowątkowym, podczas przetwarzania TheHelperklasy, może faktycznie buforować wartość falsew jakimś rejestrze lub ramce stosu na początku Body()metody i nigdy nie odświeża jej do momentu zakończenia metody. Dzieje się tak dlatego, że NIE MA GWARANCJI, że wątek i metoda NIE zakończą się przed wykonaniem "= true", więc jeśli nie ma gwarancji, to dlaczego nie zbuforować ich i nie zwiększyć wydajności odczytu obiektu sterty raz zamiast czytać go za każdym razem iteracja.

Właśnie dlatego volatileistnieje słowo kluczowe .

Aby ta klasa pomocnicza była poprawna odrobinę lepiej 1) w środowiskach wielowątkowych, powinna mieć:

    public volatile bool isComplete = false;

ale oczywiście, ponieważ jest to kod generowany automatycznie, nie możesz go dodać. Lepszym podejściem byłoby dodanie kilku lock()znaków wokół odczytów i zapisów isCompletedlub użycie innych gotowych do użycia narzędzi do synchronizacji lub obsługi wątków / zadań, zamiast próbować to zrobić na czysto metalowym (co nie będzie czystym metalem, ponieważ jest to C # w CLR z GC, JIT i (..)).

Różnica w trybie debugowania występuje prawdopodobnie dlatego, że w trybie debugowania wykluczonych jest wiele optymalizacji, więc można, no cóż, debugować kod widoczny na ekranie. W związku z tym while (!isComplete)nie jest zoptymalizowany, więc można tam ustawić punkt przerwania, i dlatego isCompletenie jest agresywnie buforowany w rejestrze lub stosie na początku metody i jest odczytywany z obiektu na stercie przy każdej iteracji pętli.

BTW. To tylko moje przypuszczenia. Nawet nie próbowałem tego skompilować.

BTW. To nie wygląda na błąd; bardziej przypomina bardzo niejasny efekt uboczny. Ponadto, jeśli mam rację, może to być brak języka - C # powinien umożliwiać umieszczanie słowa kluczowego „volatile” na zmiennych lokalnych, które są przechwytywane i promowane do pól członkowskich w domknięciach.

1) Zobacz poniżej komentarz Erica Lipperta na temat volatilei / lub ten bardzo interesujący artykuł pokazujący poziomy złożoności związane z zapewnieniem, że kod, na którym volatilesię opieramy, jest bezpieczny ... uh, dobrze ... och, powiedzmy OK.


2
@EricLippert: whoa, bardzo dziękuję za potwierdzenie tego tak szybko! Jak myślisz, czy jest jakaś szansa, że ​​w jakiejś przyszłej wersji możemy uzyskać volatileopcję przechwytywania do zamknięcia zmiennych lokalnych? Wyobrażam sobie, że może to być trochę trudne do przetworzenia przez kompilator ..
quetzalcoatl

7
@quetzalcoatl: Nie liczyłbym na to, że ta funkcja zostanie dodana w najbliższym czasie. Jest to rodzaj kodowania, który chciałbyś zniechęcać , a nie ułatwiać . Poza tym niestabilność niekoniecznie rozwiązuje każdy problem. Oto przykład, w którym wszystko jest niestabilne, a program nadal jest zły; czy możesz znaleźć błąd? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
Zrozumiany. Poddaję się próbom zrozumienia optymalizacji wielowątkowych ... to szaleństwo, jakie to skomplikowane.
Pomiędzy

10
@Pikoh: Ponownie myśl jak optymalizator. Masz zmienną, która jest zwiększana, ale nigdy nie jest odczytywana. Zmienną, która nigdy nie jest odczytywana, można całkowicie usunąć.
Eric Lippert

4
@EricLippert teraz mój umysł wykonał kliknięcie. Ten wątek był bardzo pouczający, bardzo dziękuję.
Pikoh

82

Odpowiedź Quetzalcoatla jest poprawna. Aby rzucić więcej światła na to:

Kompilator C # i jitter CLR mogą wykonywać wiele optymalizacji, które zakładają, że bieżący wątek jest jedynym działającym wątkiem. Jeśli te optymalizacje powodują, że program jest nieprawidłowy w świecie, w którym bieżący wątek nie jest jedynym działającym wątkiem , oznacza to twój problem . Jesteś zobowiązany napisać wielowątkowych programów, które mówią, że kompilator i jitter co szalony wielowątkowy rzeczy robisz.

W tym konkretnym przypadku jitter jest dozwolony - ale nie jest wymagany - aby zaobserwować, że zmienna jest niezmieniona przez ciało pętli i w związku z tym stwierdzić, że - z założenia jest to jedyny działający wątek - zmienna nigdy się nie zmieni. Jeśli nigdy się nie zmieni, to zmienna musi być sprawdzona pod kątem prawdy raz , a nie za każdym razem w pętli. I tak właśnie się dzieje.

Jak to rozwiązać? Nie pisz programów wielowątkowych . Wielowątkowość jest niezwykle trudna do osiągnięcia, nawet dla ekspertów. Jeśli musisz, użyj mechanizmów najwyższego poziomu, aby osiągnąć swój cel . Rozwiązaniem tutaj nie jest uczynienie zmiennej zmienną. Rozwiązaniem jest tutaj napisanie anulowalnego zadania i użycie mechanizmu anulowania biblioteki zadań równoległych . Pozwól TPL martwić się poprawnością logiki wątków i prawidłowym wysłaniem anulowania przez wątki.


1
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Madara's Ghost

14

Podłączyłem się do uruchomionego procesu i stwierdziłem (jeśli nie popełniłem błędów, nie jestem z tym zbyt doświadczony), że Threadmetoda jest przetłumaczona na to:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(który zawiera wartość isComplete) jest ładowany po raz pierwszy i nigdy nie jest odświeżany.


8

Właściwie nie jest to odpowiedź, ale aby rzucić więcej światła na ten problem:

Wydaje się, że problem pojawia się, gdy ijest zadeklarowany w treści lambda i jest odczytywany tylko w wyrażeniu przypisania. W przeciwnym razie kod działa dobrze w trybie wydania:

  1. i zadeklarowane poza treścią lambda:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i nie jest czytane w wyrażeniu przypisania:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i czyta się też gdzie indziej:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

Założę się, że jakiś kompilator lub optymalizacja JIT ijest zepsuta. Ktoś mądrzejszy ode mnie prawdopodobnie będzie w stanie rzucić więcej światła na ten problem.

Niemniej jednak nie martwiłbym się tym zbytnio, ponieważ nie widzę, gdzie podobny kod faktycznie służyłby jakiemukolwiek celowi.


1
zobacz moją odpowiedź, jestem prawie pewien, że chodzi o słowo kluczowe „volatile”, którego nie można dodać do zmiennej lokalnej (która w rzeczywistości zostaje później awansowana do pola członka w zamknięciu) ..
quetzalcoatl
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.