Speicherzugriff bei Threads



  • Da ist gar nichts benign dran. qwas hat das schon richtig beschrieben.



  • Das Hauptproblem in der Praxis wird hier sein, dass der Compiler nicht sieht, dass der Wert der Variable von anderen Threads verändert werden kann und daher in einem Register zwischenspeichern kann. Würdest du während dem Schleifendurchlauf den Zähler in einem anderen Thread wieder zurücksetzen, würde das die Schleife wahrscheinlich nicht jucken (je nach Compiler, Einstellungen und was so in der Schleife passiert). Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt, weil der Compiler gar nicht einsieht, warum der sich je ändern sollte.

    Denkt man deine Behauptung weiter, dann wuerde es auch nicht helfen, die int-Variable mit einer CriticalSection oder aehnliches zu schuetzen? Da jeder einzelne Thread die Variable in einem Register zwischenspeichern kann?

    ansonsten: http://herbsutter.com/2014/01/13/gotw-95-solution-thread-safety-and-synchronization/



  • knivil schrieb:

    Denkt man deine Behauptung weiter, dann wuerde es auch nicht helfen, die int-Variable mit einer CriticalSection oder aehnliches zu schuetzen? Da jeder einzelne Thread die Variable in einem Register zwischenspeichern kann?

    Natürlich hilft das. Solche Operationen bilden eine memory barrier, daher ist es sicher, wenn alle deine Zugriffe innerhalb deiner critical section passieren. Das wird dir zum einen von nicht-C++-Funktionen wie pthread_mutex_lock() garantiert, aber auch vom C++-Standard für mutexes allgemein.



  • Hlymur schrieb:

    Heyhey @ all,

    ich habe eine Frage, welche ich mir nicht auf anhieb selber beantworten konnte (d.h. ich war auch zu dumm, um bei Google etwas gescheites zufinden).

    Es geht um folgendes Problem:
    Ich habe 2 Threads und eine globale unsigned int Vaiable.
    Im ersten Thread hab ich eine for()-Schleife, die die globale Variable als Laufvaraible verwendet und in jedem durchlauf ieine lange-komplexe Rechnung macht.
    Mein zweiter Thread soll quasi den "Status" anzeigen, d.h. ich will in dem Thread auf die globale Variable zugreifen, sie Lesen un dann irgendwie anzeigen lassen. Ich hab das auch mal getestet und es lief, jetzt weiß ich nur nicht, ob dass ein netter Zufall war oder ob das grundsätzlich ok ist.
    Können also 2 Threads auf dieselbe Varaible zugreifen(wobei nur ein Thread schreibt) ?

    Danke und mfG
    Hlymur

    unsigned int ist ok, das brauchst du nicht schützen.
    Es ist nicht so, dass der Computer byte für byte schreibt, sondern er arbeitet auf einer bestimmten Wortbreite, z.B. 32bit.
    Kannst die Variable ja noch volatile machen, dann sollte der Compiler die nicht in einem Register halten.



  • @knivil
    Bei einem Compiler der EnterCriticalSection/LeaveCriticalSection nicht als Memory-Barriers behandelt UND grundsätzlich ungeeignet ist korrekten multithreaded Code zu generieren könnte es theoretisch Probleme geben.

    Allerdings nicht das Problem dass ne Variable im Register gehalten wird und nicht zurückgeschrieben/nicht neu gelesen wird.
    Und zwar deswegen nicht, weil der Compiler den Code von EnterCriticalSection/LeaveCriticalSection nicht kennt - sind ja Kernel Calls.

    Daher muss er davon ausgehen, dass EnterCriticalSection/LeaveCriticalSection sämtliche Speicherstellen lesen oder schreiben könnte deren Adresse ihnen bekannte sein könnte.
    D.h. sämtliche globalen Variablen müssen vor einem so einem "Aufruf ins Unbekannte" zurpckgeschrieben werden, und danach neu eingelesen.
    Weiters auch sämtliche Variablen deren Adresse mal in eine globale Variable geschrieben wurde. Und auch alle deren Adresse einer anderen solchen "unbekannten" Funktion übergeben wurde.
    Bzw. alles was über solche Parameter/Werte von globalen Variablen erreichbar ist.

    DAS Problem kann es also schonmal nicht geben.

    -----

    Dafür kann es andere Probleme geben. Und zwar darf ein Compiler ja die "as if" Regel ausnutzen. Und ein Compiler der keine Threads unterstützt kann da einige Optimierungen machen, die einem "as if" Compiler strengstens verboten sind.

    Folgende Transformation wäre z.B. für einen "keine Threads" Compiler OK:

    int a, b, c, d, someGlobal; //
    
    void Foo()
    {
        if (someGlobal < 10)
        {
            someGlobal = 123;
            a++;
        }
        else if (someGlobal < 50)
        {
            someGlobal = 123;
            b++;
        }
        else if (someGlobal != 123)
        {
            someGlobal = 123;
            c++;
        }
        else
        {
            d++;
        }
    
        assert(someGlobal == 123);
    }
    
    // ->
    void Foo()
    {
        if (someGlobal < 10)
            a++;
        else if (someGlobal < 50)
            b++;
        else if (someGlobal != 123)
            c++;
        else
            d++;
    
        someGlobal = 123;
    
        assert(someGlobal == 123);
    }
    

    Ohne Threads ist der "zusätzliche" Schreibzugriff auf someGlobal , der passiert wenn der Code im letzten "else" Zweig landet, OK. Nachher steht sowieso immer 123 drinnen, und der Schreibzugriff als solcher ist nicht beobachtbar (die Variable ist ja nicht volatile ).

    Mit Threads wird hier aber ne Variable beschrieben die im Originalprogramm nicht beschrieben wurde. Und das ist nicht OK, selbst wenn man den selben Wert drüberschreibt den die Variable davor schon hatte.

    Anderes, ähnliches Beispiel:

    void Bar(char* someArray)
    {
        for (size_t i = 0; i < 1000; i++)
            if (i != 42)
                someArray[i] = 0;
    }
    
    ->
    
    void Bar(char* someArray)
    {
        char bak = someArray[42];
        memset(someArray, 0, 1000); // VIEL schneller als jedes char einzeln zu setzen
        someArray[42] = bak;
    }
    

    Auch hier wieder: ohne Threads OK, gibt ja keinen der zwischenzeitliche Änderungen von non-volatile Speicherstellen beobachten könnte.
    Mit Threads aber nicht OK, weil eben was geschrieben wird was das original Programm nicht schreibt.



  • fsdfsdfs schrieb:

    unsigned int ist ok, das brauchst du nicht schützen.
    Es ist nicht so, dass der Computer byte für byte schreibt, sondern er arbeitet auf einer bestimmten Wortbreite, z.B. 32bit.
    Kannst die Variable ja noch volatile machen, dann sollte der Compiler die nicht in einem Register halten.

    Das ist in der Praxis auf vielen Systemen so, ja.
    Aber nicht auf allen.
    unsigned int kann durchaus 32 Bit sein, obwohl der Datenbus vielleicht nur 16 Bit breit ist.

    Und vom Standard her ist es schonmal gar nicht abgedeckt.



  • fsdfsdfs schrieb:

    unsigned int ist ok, das brauchst du nicht schützen.

    Ich dachte Herb Sutter & Co hätten diese Meinung ausgerottet.
    Ich empfehle How to miscompile programs with "benign" data races. Da wird erklärt, warum Data Races böse sind, selbst wenn 2 Threads denselben Wert in eine Variable schreiben, selbst auf x86 mit einem int.



  • unsigned int ist ok, das brauchst du nicht schützen

    Nun, ich hoffte mein Link greift dem vorweg. Hat sich aber niemand durchgelesen.

    @hustbaer: Jaja, ich weiss das alles. Die Frage ist nur, warum sind Funktionsaufrufe anders im Vergleich zu normalen Variablenzugriffen? Warum nicht vor jedem Lesezugriff eine Memorybarrier, wobei moeglichst viele wegoptimiert werden, da die meisten beweisbar unnoetig sind.

    Die Sache ist die:

    Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt

    Ich habe damals (lange vor C++11) echt probiert, genau das zu erreichen. Soweit ich mich erinnern kann, hat es nicht geklappt.

    Zu der Transformation: Leider kenne ich mich mit C++11 nicht in den Feinheiten aus. Habe den Standard noch nicht gelesen. Nur im letzten else-Zweig wird someGlobal nicht beschrieben, sonst immer. D.h. die Transformation ist so (vielleicht) nicht erlaubt, aber nur weil es sich an der Stelle um d++ anders verhaelt. Andere Transformationen bezueglich den ersten if-Bedingungen koennten aber erlaubt sein.



  • knivil schrieb:

    @hustbaer: Jaja, ich weiss das alles.

    Wenn du das alles weisst, dann hast du's offensichtlich nicht verstanden.

    knivil schrieb:

    Die Frage ist nur, warum sind Funktionsaufrufe anders im Vergleich zu normalen Variablenzugriffen? Warum nicht vor jedem Lesezugriff eine Memorybarrier, wobei moeglichst viele wegoptimiert werden, da die meisten beweisbar unnoetig sind.

    Achje, das hab ich doch schon erklärt...

    Weil eben die meisten gerade nicht "beweisbar unnötig" sind. Ausser man verbietet eben Data-Races. Was genau das ist was das der C++ Standard mit seinem Memory Model macht.

    Es gibt schon so viel zu viele Stellen wo - auch mit den relativ "schwachen" Garantien die der neue C++ Standard bietet - Werte zurückgeschrieben oder neu gelesen werden müssen. Sogar ganz ohne Multithreading. Jeder Aufruf in eine DLL/SO, fast jeder virtual Aufruf oder Aufruf über Funktionszeiger. Und jeder Aufruf einer Funktion wo der Code-Generator aus welchen Gründen auch immer die aufgerufene Funktion nicht genauestens unter die Lupe nehmen kann. Was bei aktuellen Compilern vermutlich jeder Aufruf sein wird der nicht inline erweitert wird.

    Falls du anderer Meinung bist, dann liefere doch bitte eine exakte Definition. Also wie würde dein Memory-Model aussehen? Wenn du 'was brauchbares zusammenbringst ist die Ruhm & Ehre in der Programmierer-Community sicher.

    Die Sache ist die:

    Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt

    Ich habe damals (lange vor C++11) echt probiert, genau das zu erreichen. Soweit ich mich erinnern kann, hat es nicht geklappt.

    Die allermeisten (*) Funktionen um etwas "anzuzeigen" sind welche die der Compiler nicht "sehen" kann. Daher kann erkeine Annahmen darüber treffen, und muss sie daher wie EnterCriticalSection/LeaveCriticalSection behandeln.
    Ist doch irgendwie logisch.

    Ein einfaches

    while (var)
        othervar++;
    

    hat sogar schon VC6 zu

    reg = var;
    while (reg)
        othervar++;
    

    optimiert. Das weiss ich ganz sicher, weil ich es ausprobiert habe.
    Sobald du natürlich ein printf(var) in den Loop reinschreibst ist der Spuk vorbei. Weil printf irgendwo nen Kernel-Call enthält. Und ich kenne keinen Compiler der Annahmen darüber trifft was welche Kernel-Calls tun dürfen und was nicht.

    *: Wobei "allermeisten" hier quasi mit "alle" gleichzusetzen ist. Klar, Funktionen die direkt in den Video-Speicher schreiben gingen vermutlich. Und mit solchen Funktionen könnte man es vermutlich sogar hinbekommen. Ist aber eigentlich vollkommen uninteressant ob man so einen Fall real konstuieren kann oder nicht.



  • hustbaer schrieb:

    Ein einfaches

    while (var)
        othervar++;
    

    hat sogar schon VC6 zu

    reg = var;
    while (reg)
        othervar++;
    

    optimiert. Das weiss ich ganz sicher, weil ich es ausprobiert habe.
    Sobald du natürlich ein printf(var) in den Loop reinschreibst ist der Spuk vorbei. Weil printf irgendwo nen Kernel-Call enthält. Und ich kenne keinen Compiler der Annahmen darüber trifft was welche Kernel-Calls tun dürfen und was nicht.

    Ich dachte, der Spuk sei vorbei, weil der Compiler annehmen muss, daß das Register von printf zerstört wird.



  • Falls du anderer Meinung bist, dann liefere doch bitte eine exakte Definition. Also wie würde dein Memory-Model aussehen? Wenn du 'was brauchbares zusammenbringst ist die Ruhm & Ehre in der Programmierer-Community sicher.

    Jetzt geht das wieder los ... 🙄 Und VC 6 als Referenz mit seiner Standardkonformitaet halte ich fuer fragwuerdig. Damals habe ich gcc 3.irgendwas benutzt, pthreads und habe mehr als nichts gemacht, die Variable muss direkt oder indirekt beobachtbar sein, sonst hats keinen Sinn. Und natuerlich kann das Programm den Inhalt der Register sichern und wiederherstellen ... Aber ich sehe wo das wieder hinfuehrt, kein Bock drauf.



  • fsdfsdfs schrieb:

    unsigned int ist ok, das brauchst du nicht schützen.
    Es ist nicht so, dass der Computer byte für byte schreibt, sondern er arbeitet auf einer bestimmten Wortbreite, z.B. 32bit.
    Kannst die Variable ja noch volatile machen, dann sollte der Compiler die nicht in einem Register halten.

    Falsch. Folgendes Programm:

    #include <stdio.h>
    #include <pthread.h>
    
    #define LOOPS 1000000
    
    int counter = 0;
    
    void* inc(void* p)
    {
        unsigned n;
        for (n = 0; n < LOOPS; ++n)
            ++counter;
    }
    
    void* dec(void* p)
    {
        unsigned n;
        for (n = 0; n < LOOPS; ++n)
            --counter;
    }
    
    int main()
    {
        pthread_t incThread, decThread;
    
        pthread_create(&incThread, 0, inc, 0);
        pthread_create(&decThread, 0, dec, 0);
        pthread_join(incThread, 0);
        pthread_join(decThread, 0);
        printf("%d\n", counter);
    }
    

    Sollte eigentlich immer 0 ausgeben, da ja der eine Thread die Variable eine Million mal inkrementiert und der andere eine Million mal dekrementiert. Ich habe das Programm unter debian auf VMWare mit einem Core (x86) übersetzt und laufen gelassen. Es eignet sich aber eher als Zufallszahlengenerator.

    Übrigens habe ich das mit: gcc -o foo -lpthread foo.c übersetzt. Wird das mit -O2, also optimiert übersetzt, dann kommt immer 0 raus. Was aber nicht wirklich überraschend ist, da so eine Schleife leicht zu optimieren ist.



  • Hä?
    Ja und?

    Was zeigt das Programm so? oder was soll es zeigen?
    Und infwiefern verneint es die volatile-Aussage?

    So zeigt dein Programm nur, dass mit Concurrency nicht zu spaßen ist, wenn man nicht aufpasst... aber mehr auch nicht.

    Und dass mit Optimierungen der Compiler die Schleife zu einem Konstanten Ausdruck zusammenfasst ist schön, aber auch nicht aussagekräftig.



  • Skym0sh0 schrieb:

    Und infwiefern verneint es die volatile-Aussage?

    volatile hindert den Compiler im Prinzip daran, den Wert in einem Register zu halten. Es garantiert aber nicht Atomizität; obiges Beispiel wär auch mit volatile undefiniertes Verhalten.



  • Skym0sh0 schrieb:

    Hä?
    Ja und?

    Was zeigt das Programm so? oder was soll es zeigen?
    Und infwiefern verneint es die volatile-Aussage?

    Das habe ich bewusst zitiert:

    fsdfsdfs schrieb:

    unsigned int ist ok, das brauchst du nicht schützen.

    Und genau diese Aussage habe ich widerlegt. Ich habe mich auf keine volatile-Aussage bezogen. Bei der Diskussion geht es doch darum, ob man eine globale Variable beim parallelen Zugriff schützen muss. Und die Antwort lautet: ja.

    Im übrigen ändert sich nichts, wenn wenn ich die Variable volatile mache.



  • Also, ich hatte nie Probleme, solches Verhalten nachzubilden - das hier reicht schon:

    #include <iostream>
    #include <thread>
    
    int main(int argc,char** argv)
    {
      std::size_t counter=0;
      std::thread([&]() {for(;;)counter++;}).detach();
    
      for(;;)
      {
        std::cout << counter << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
      }
    }
    

    Gibt bei mir mit gcc 4.8.1 und -O2 immer 0 aus.

    Hab auch sonst schon oft ausprobiert, was passiert, wenn ich gemeinsam verwendete Variablen nicht angemessen schütze. Die Antwort ist oft einfach: es passiert nichts mehr, irgendein wichtiger Thread optimiert sich quasi selbst weg und hält den Betrieb auf.


Anmelden zum Antworten