Datenkonsistenz in Multithreadedanwendungen



  • krümelkacker schrieb:

    Ich verstehe die Frage nicht ganz, möchte aber auf ein paar Dinge hinweisen: Es gibt Sequenzpunkte. Der Compiler kann nur anhand der as-if Regel Instruktionen umsortieren. Und wenn irgendwo ein Funktionsaufruf dabei ist, der ja wer weiß was für Seiteneffekte haben kann, dann wird da auch nichts umsortiert.

    Da würde ich nciht meine Hand drauf verwetten. Mit Inlinig der Seiteneffekte kann da durchaus kräftig rumsortiert werden, auf jeden Fall so, dass die "as if" Regel nicht verletzt wird, aber ein anderer Thread dennoch seltsame Zwischenwerte sehen kann.

    Wen das Thema interessiert, Herb Sutter hat einiges dazu zu sagen:
    http://herbsutter.com/2010/09/24/effective-concurrency-know-when-to-use-an-active-object-instead-of-a-mutex/
    und alle Links dort.



  • dot schrieb:

    Wobei ich mir nicht ganz sicher bin ob der Code da ohne volatile nicht vielleicht doch undefiniertes Verhalten wär.

    Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre und b) gibt es dazu einige Quellen im Internet, die das Gleiche aussagen.

    Ich formuliere meine Frage noch einmal um:

    Im oben genannten Beispiel könnte der Compiler bei der thread1 Funktion auf die Idee kommen a in einem Register zu halten. Dann würde thread1 in einer Endlosschleife laufen, da er ja die Änderung von a in thread2 dann nicht mitbekommen würde. Irgendein Mechanismus muss nun also existieren, damit o.g. Beispiel funktioniert. Ich wüsste gerne welcher. Erkennt der Compiler, dass es sich hierbei um eine Critical Section handelt und liest deshalb Variable a immer neu aus dem Speicher oder geschieht das durch einen anderen Mechanismus?



  • StefanS schrieb:

    dot schrieb:

    Wobei ich mir nicht ganz sicher bin ob der Code da ohne volatile nicht vielleicht doch undefiniertes Verhalten wär.

    Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre

    Ein char hat immer genau acht Bit. Das weiß ich aus eigener Erfahrung, denn bisher wars noch immer so.



  • StefanS schrieb:

    Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre und b) gibt es dazu einige Quellen im Internet, die das Gleiche aussagen.

    Das heißt noch lange nicht dass der C++-Standard garantiert dass das immer so funktioniert...



  • Michael E. schrieb:

    Ein char hat immer genau acht Bit. Das weiß ich aus eigener Erfahrung, denn bisher wars noch immer so.

    Quelle: http://drdobbs.com/cpp/184403766

    Inside a critical section defined by a mutex, only one thread has access. Consequently, inside a critical section, the executing code has single-threaded semantics. The controlled variable is not volatile anymore — you can remove the volatile qualifier.

    Autor ist übrigens Andrei Alexandrescu

    dot schrieb:

    StefanS schrieb:

    Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre und b) gibt es dazu einige Quellen im Internet, die das Gleiche aussagen.

    Das heißt noch lange nicht dass der C++-Standard garantiert dass das immer so funktioniert...

    C++ kennt von Natur aus keine Threads. Aber wie gesagt siehe Quelle oben



  • StefanS schrieb:

    Michael E. schrieb:

    Ein char hat immer genau acht Bit. Das weiß ich aus eigener Erfahrung, denn bisher wars noch immer so.

    Quelle: http://drdobbs.com/cpp/184403766

    Inside a critical section defined by a mutex, only one thread has access. Consequently, inside a critical section, the executing code has single-threaded semantics. The controlled variable is not volatile anymore — you can remove the volatile qualifier.

    Zum Thema hab ich gar nichts gesagt. Ich hab nur gesagt, dass Punkt 1 deiner Begründung wertlos ist.



  • Zum Thema hab ich gar nichts gesagt. Ich hab nur gesagt, dass Punkt 1 deiner Begründung wertlos ist.

    Ja für Beweisfetischisten ist so eine Aussage wertlos.



  • Mit Fetischismus hat das noch nicht mal viel zu tun. Beim Standard hat man häufig undefiniertes Verhalten, ohne es durch Ausprobieren zu merken.



  • Stefans schrieb:

    Bei c/C++ multithreaded Anwendungen kann bzw. sollte man ja auf das volatile keyword verzichten. Wer sorgt dann allerdings dafür, dass die Daten weiterhin synchronisiert sind (in einer critical section)
    weil der Compiler ja trotzdem wild optimieren kann.

    Kann er eben nicht.
    Im Prinzip gibt es grob zwei Arten wie garantiert wird, dass es klappt.

    1. Der Compiler "kennt" Multithreading, inklusive der diversen zur Synchronisierung verwendeten Funktionen (CreateThread, EnterCriticalSection, WaitForSingleObject etc.). Dann muss er auch wissen was er wo machen darf bzw. nicht machen darf. Ich kenne allerdings kein System, wo das so laufen würde. Also nur der Vollständigkeit halber.

    2. Die zur Synchronisierung verwendeten Funktionen sind für den Compiler "undurchsichtig". Damit meine ich, der Compiler kann den Code dieser Funktionen nicht analysieren, bzw. weiss, dass er es verdammtnochmal nicht tun sollte.
      D.h. der Compiler muss damit rechnen, dass diese Funktionen alles mögliche tun können. Das ist der Knackpunkt.
      (Bei Plattformen die etwas wie DLLs oder Shared-Objects unterstützen, kann man im Normalfall davon ausgehen, dass alles was in einer DLL bzw. einem Shared-Object implementiert ist, für den Compiler "undurchsichtig" ist.)

    Einfaches Beispiel:

    void UndurchsichtigeFunktion();
    
    extern int a; // global
    
    void Foo()
    {
        for (a = 0; a < 100; a++)
            UndurchsichtigeFunktion();
    }
    

    Der Compiler muss hier a vor jedem Aufruf von UndurchsichtigeFunktion() in den Speicher zurück schreiben, und nach jedem Aufruf neu aus dem Speicher laden. UndurchsichtigeFunktion() könnte ja z.B. so aussehen:

    extern int a;
    void UndurchsichtigeFunktion()
    {
        printf("a = %d\n", a);
        if (a == 23)
            a = 42;
    }
    

    OK, globale Variablen sind ein Thema, aber es gibt ja auch noch andere Möglichkeiten. z.B.

    void Foo()
    {
        int a = 0; // lokal
        CreateThread(..., MyThreadFunction, &a);
        while (1)
        {
            Sleep(123);
            UndurchsichtigeFunktion1(); // macht z.B. EnterCriticalSection(irgendwasGlobales);, aber das weiss der Compiler nicht
            if (a == 42)
                break;
            UndurchsichtigeFunktion2(); // macht z.B. LeaveCriticalSection(irgendwasGlobales);, aber das weiss der Compiler auch nicht
        }
        UndurchsichtigeFunktion2();
    }
    

    Nun, da CreateThread auch undurchsichtig ist, muss der Compiler mit folgendem Rechnen:

    int* global_a_ptr = 0;
    
    void CreateThread(... int* a ...)
    {
        global_a_ptr = a;
    }
    
    void UndurchsichtigeFunktion1()
    {
        (*global_a_ptr)++;
    }
    
    void UndurchsichtigeFunktion2()
    {
        (*global_a_ptr)++;
    }
    

    ----

    Soweit ist also mal, über Mechanismen die mit Multithreading gar nix zu tun haben, sichergestellt, dass der Compiler nicht wild reordern/optimieren kann.

    Und um den Rest, also die nötigen Memory-Fences etc., kümmern sich dann die diversen zur Synchronisierung verwendeten Funktionen selbst.

    ps: kritisch könnte es werden, wenn man
    a) Synchronisierung über Inline-Assembler macht und
    b) der Compiler Inline-Assembler nicht als undurchsichtig behandelt (sondern analysiert) aber
    c) nicht mitbekommt dass hier ein Thread-Synchronisierungs-Konstrukt verwendet wird

    AFAIK muss man da z.B. bei GCC aufpassen, und spezielle "Hinweise" in den Inline-Assembler-Code reintun, damit GCC weiss was abgeht.



  • pumuckl schrieb:

    krümelkacker schrieb:

    Ich verstehe die Frage nicht ganz, möchte aber auf ein paar Dinge hinweisen: Es gibt Sequenzpunkte. Der Compiler kann nur anhand der as-if Regel Instruktionen umsortieren. Und wenn irgendwo ein Funktionsaufruf dabei ist, der ja wer weiß was für Seiteneffekte haben kann, dann wird da auch nichts umsortiert.

    Da würde ich nciht meine Hand drauf verwetten. Mit Inlinig der Seiteneffekte kann da durchaus kräftig rumsortiert werden, auf jeden Fall so, dass die "as if" Regel nicht verletzt wird, aber ein anderer Thread dennoch seltsame Zwischenwerte sehen kann.

    Das mit dem Inlining stimmt. Ich kann mir aber vorstellen, dass Compilerhersteller und Standardbibliotheksautoren das Locking und Unlocking der Mutexes über Funktionen bereitstellt, die garantiert nicht "geinlined" werden. Wenn Du komische Effekte siehst (seltsame Zwischenwerte) hast Du wahrscheinlich schon undefiniertes Verhalten hervorgerufen (zB wegen eines "data race"s).

    Ich würde mir jedenfalls weniger Sorgen darüber machen, was ein Compiler alles so tun und umsortieren kann und mich mehr dafür interessieren, was im kommenden Standard zum Thema "data race" stehen wird. Sofern sich Compilerhersteller und Programmierer an das halten, was im kommenden Standard dazu steht ist alles i.O. Mit Atomics und Mutexes (und deren korrekte Anwendung) ist man da auf sicherer Seite.


Anmelden zum Antworten