Pytanie Powtarzalny przykład niestabilnego użycia


Szukam odtwarzalnego przykładu, który może pokazać, jak działa zmienne słowo kluczowe. Szukam czegoś, co działa "źle" bez zmiennej (zmiennych) oznaczonych jako volatile i działa "poprawnie" z nią.

Mam na myśli przykład, który pokazuje, że kolejność operacji zapisu / odczytu podczas wykonywania jest inna niż oczekiwana, gdy zmienna nie jest oznaczona jako zmienna i nie jest różna, gdy zmienna nie jest oznaczona jako zmienna.

Myślałem, że mam przykład, ale potem z pomocą innych osób zdałem sobie sprawę, że to po prostu kawałek błędnego kodu wielowątkowego. Dlaczego volatile i MemoryBarrier nie zapobiegają zmianie kolejności operacji?

Znalazłem również link, który demonstruje efekt niestabilności na optymalizatorze, ale różni się od tego, czego szukam. Pokazuje, że wnioski o zmienne oznaczone jako zmienne nie zostaną zoptymalizowane.Jak zilustrować użycie zmiennego słowa kluczowego w języku C #

Oto, gdzie dotarłem tak daleko. Ten kod nie wykazuje żadnych oznak zmiany kolejności operacji odczytu / zapisu. Szukam jednego, który się pokaże.

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Runtime.CompilerServices;

    namespace FlipFlop
    {
        class Program
        {
            //Declaring these variables 
            static byte a;
            static byte b;

            //Track a number of iteration that it took to detect operation reordering.
            static long iterations = 0;

            static object locker = new object();

            //Indicates that operation reordering is not found yet.
            static volatile bool continueTrying = true;

            //Indicates that Check method should continue.
            static volatile bool continueChecking = true;

            static void Main(string[] args)
            {
                //Restarting test until able to catch reordering.
                while (continueTrying)
                {
                    iterations++;
                    a = 0;
                    b = 0;
                    var checker = new Task(Check);
                    var writter = new Task(Write);
                    lock (locker)
                    {
                        continueChecking = true;
                        checker.Start();

                    }
                    writter.Start();
                    checker.Wait();
                    writter.Wait();
                }
                Console.ReadKey();
            }

            static void Write()
            {
                //Writing is locked until Main will start Check() method.
                lock (locker)
                {
                    WriteInOneDirection();
                    WriteInOtherDirection();

                    //Stops spinning in the Check method.
                    continueChecking = false;
                }
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            static void WriteInOneDirection(){
                a = 1;
                b = 10;
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            static void WriteInOtherDirection()
            {
                b = 20;
                a = 2;
            }

            static void Check()
            {
                //Spins until finds operation reordering or stopped by Write method.
                while (continueChecking)
                {
                    int tempA = a;
                    int tempB = b;

                    if (tempB == 10 && tempA == 2)
                    {
                        continueTrying = false;
                        Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB);
                        Console.WriteLine("In " + iterations + " iterations.");
                        break;
                    }
                }
            }
        }
    }

Edytować:

Rozumiem optymalizację, która powoduje, że zmiana kolejności może pochodzić z JITer lub samego sprzętu. Mogę przeformułować moje pytanie. Czy procesory JITer lub x86 zmieniają kolejność operacji odczytu / zapisu I czy istnieje sposób, aby zademonstrować to w języku C #?


11
2018-05-28 21:33


pochodzenie


Łatwo jest (na x86) podać przykład, w którym ma to znaczenie, wymuszając czytanie; czy to wystarczy? A może zwracasz się tylko do zamawiania? - Marc Gravell♦
(tutaj, na zmuszaniu do czytania - nie tak jak strogiem jako zamawianiem: stackoverflow.com/questions/458173/...) - Marc Gravell♦
Jak już powiedziałem, widziałem te posty. Interesuje mnie przykład zmiany kolejności. - Dennis
W przykładzie MSDN nie chodzi o zmianę kolejności. Chodzi o optymalizację z "ukończonego" pola. - Dennis
Możesz także skupić się na układach AMD i Itanium (iA64): albahari.com/threading/part4.aspx#_The_volatile_keyword - Marc Gravell♦


Odpowiedzi:


Dokładna semantyka lotny to szczegół implementacji jittera. Kompilator wysyła instrukcję Opcodes.Volatile IL, gdzie zawsze uzyskujesz dostęp do zmiennej, która została uznana za lotną. Wykonuje pewne sprawdzenie w celu sprawdzenia, czy typ zmiennej jest poprawny, nie można zadeklarować typów wartości większych niż 4 bajty, ale jest to miejsce, w którym zatrzymuje się blokowanie.

Specyfikacja języka C # definiuje zachowanie lotny, cytowane tutaj Eric Lippert. Semantyka 'release' i 'acquire' to coś, co ma sens tylko w rdzeniu procesora o słabym modelu pamięci. Tego rodzaju procesory nie radzą sobie dobrze na rynku, prawdopodobnie dlatego, że mają ogromny problem z programowaniem. Szanse, że twój kod kiedykolwiek będzie działał na tytanie, są niewielkie lub żadne.

To, co jest szczególnie złe w definicji specyfikacji języka C #, to to, że w ogóle nie wspomina o tym naprawdę dzieje się. Deklarowanie zmiennej lotności zapobiega optymalizacji jittera przez optymalizację kodu w celu zapisania zmiennej w rejestrze procesora. Właśnie dlatego zwisa kod, który Marc łączył. Stanie się tak tylko z obecnym jitterem x86, kolejną silną wskazówką lotny jest naprawdę szczegółem implementacji jittera.

Zła semantyka lotny ma bogatą historię, pochodzi z języka C. Którego generatory kodu mają również wiele problemów z uzyskaniem tego samego. Oto interesujące zgłoś o tym (pdf). Pochodzi z 2008 roku, z dobrych 30+ lat szansy, aby to naprawić. Lub źle, to idzie w górę, kiedy optymalizator kodu zapomina o zmiennej niestabilności. Nieoptymalizowany kod nigdy nie ma z tym problemu. Warto zauważyć, że fluktuacja w wersji ".NET (open source)" (SSLI20) całkowicie ignoruje instrukcję IL. Można również argumentować, że obecne zachowanie jittera x86 jest błędem. Myślę, że tak jest, nie jest łatwo wpaść w tryb awaryjny. Ale nikt nie może się tak spierać jest błąd.

Napis znajduje się na ścianie, tylko zadeklaruj zmienną zmienną, jeśli jest przechowywany w rejestrze odwzorowanym w pamięci. Pierwotny zamiar słowa kluczowego. Szanse, które napotkasz na takie użycie w języku C #, powinny być znikome, a kod taki należy do sterownika urządzenia. I ponad wszystko, nigdy załóżmy, że jest to przydatne w scenariuszu wielowątkowym.


17
2018-05-28 22:38



dobre napisanie. Mam tendencję do Interlocked koniec volatile siebie - przynajmniej mogę w pełni zdefiniować zachowanie; - Marc Gravell♦
Dobra odpowiedź. Joe Duffy także ma opinię: Lotny jest zły - Henk Holterman
To nie odpowiada na moje pytanie. Szukam przykładu, który pokaże, że lotna operacja "zapewnia, że ​​operacje pamięciowe z zewnętrznymi efektami ubocznymi nie zostaną ponownie uporządkowane", jak mówi Joe Duffy. - Dennis
Cóż, zrobiłem. Najpierw znajdź maszynę z procesorem i słabym modelem pamięci. Jeden z Itanitum nie powinien być zbyt trudny do znalezienia na kupie złomu. Poczekaj do zimy, słyszałem, że są dobre grzejniki kosmiczne. Rdzenie ARM są też słabe, jak sądzę, będą wymagały telefonu Windows 7 z dwurdzeniowym procesorem. Będziesz musiał na to także poczekać. Najprawdopodobniej jest to klon iPada Microsoftu. - Hans Passant


Możesz użyć tego przykładu, aby zademonstrować różne zachowania z i bez volatile. Ten przykład musi zostać skompilowany przy użyciu wersji Release i uruchomiony poza debuggerem1. Eksperyment przez dodanie i usunięcie volatile słowo kluczowe do stop flaga.

To, co się tutaj dzieje, to to, że przeczytałem stop w while Pętla zostaje zmieniona tak, że występuje przed pętlą, jeśli volatile jest pominięty. Zapobiega to zakończeniu wątku nawet po ustawieniu głównego wątku stop flaga do true.

class Program
{
    static bool stop = false;

    public static void Main(string[] args)
    {
        var t = new Thread(() =>
        {
            Console.WriteLine("thread begin");
            bool toggle = false;
            while (!stop)
            {
                toggle = !toggle;
            }
            Console.WriteLine("thread end");
        });
        t.Start();
        Thread.Sleep(1000);
        stop = true;
        Console.WriteLine("stop = true");
        Console.WriteLine("waiting...");

        // The Join call should return almost immediately.
        // With volatile it DOES.
        // Without volatile it does NOT.
        t.Join(); 
    }
}

Należy również zauważyć, że subtelne zmiany w tym przykładzie mogą zmniejszyć prawdopodobieństwo odtwarzalności. Na przykład dodanie Thread.Sleep (być może do symulacji przeplatania wątków) sam wprowadzi barierę pamięci, a tym samym podobną semantykę volatile słowo kluczowe. podejrzewam Console.WriteLine wprowadza niejawne bariery pamięciowe lub w inny sposób zapobiega stosowaniu przez jitter operacji zmiany kolejności instrukcji. Miej to na uwadze, jeśli zaczniesz zbytnio mieszać się do tego przykładu.


1Uważam, że wersja ramowa wcześniejsza niż 2.0 nie uwzględnia tej optymalizacji zmiany kolejności. Oznacza to, że powinieneś być w stanie odtworzyć to zachowanie w wersji 2.0 lub wyższej, ale nie w starszych wersjach.


4
2018-05-28 22:58