Frage

Bisher habe ich einige sehr einfache multithreaded Code geschrieben, und ich habe immer bewusst, dass es jederzeit ein Kontextschalter rechts in der Mitte sein könnte, was ich tue, so habe ich immer Zugang bewacht die gemeinsam genutzten Variablen über eine CCriticalSection Klasse, die den kritischen Abschnitt auf Konstruktion und lässt sie auf die Zerstörung eintritt. Ich weiß, das ziemlich aggressiv ist und ich betreten und verlassen kritische Abschnitte recht häufig und manchmal in grober Weise (zB zu Beginn einer Funktion, wenn ich die CCriticalSection innerhalb eines engeren Codeblock setzen könnte), aber mein Code nicht abstürzt und es läuft schnell genug .

Bei der Arbeit meiner multithreaded Code Bedürfnisse sein, ein festen, nur Sperren / Synchronisieren auf der untersten Ebene erforderlich.

Bei der Arbeit war ich versucht, etwas multithreaded Code zu debuggen, und ich kam in diesem:

EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);

Nun m_bSomeVariable ist ein Win32-BOOL (nicht flüchtig), die soweit ich weiß, ist, definiert ein int zu sein, und auf x86 Lesen und diese Werte schreiben, ist ein einzelner Befehl, und da Kontextwechsel treten an einer Befehlsgrenze dann gibt es keine Notwendigkeit, diesen Vorgang mit einem kritischen Abschnitt zum Synchronisieren.

Ich habe einige mehr Forschung online zu sehen, ob dieser Vorgang nicht Synchronisation brauchte, und ich kam mit zwei Szenarien bis es tut:

  1. Die CPU implementiert Ausführung außerhalb der Reihenfolge oder dem zweiten Gewinde auf einem anderen Kern ausgeführt wird und der aktualisierte Wert geschrieben wird, nicht in den RAM für den anderen Kern, um zu sehen; und
  2. Die int ist nicht 4-Byte ausgerichtet sind.

Ich glaube, Nummer 1 gelöst werden kann, die „flüchtig“ Schlüsselwort. In VS2005 und später der C ++ Compiler umgibt den Zugriff auf diese Variable Speicherbarrieren verwenden, um sicherzustellen, dass die Variable immer vollständig geschrieben / gelesen zu dem Hauptsystemspeicher, bevor es verwendet wird.

Number 2 ich nicht überprüfen kann, ich weiß nicht, warum die Byte-Ausrichtung einen Unterschied machen würde. Ich weiß nicht, den x86-Befehlssatz, aber nicht mov Notwendigkeit, eine 4-Byte-ausgerichtete Adresse angegeben werden? Wenn dies nicht tun, müssen Sie eine Kombination von Anweisungen verwenden? Das würde das Problem vorstellen.

So ...

FRAGE 1: , um das „volatile“ Schlüsselwort Ist mit (implicity Verwendung von Speicherbarrieren und anspielend an den Compiler den Code nicht zu optimieren) absolve Programmierer von der Notwendigkeit, ein 4-Byte / 8 zu synchronisieren -Byte auf x86 / x64 Variable zwischen Lese- / Schreiboperationen?

Frage 2: Gibt es die ausdrückliche Forderung, dass der Variable seiner 4-Byte / 8-Byte ausgerichtet

Ich habe in unseren Code einige mehr zu graben und die in der Klasse definierten Variablen:

class CExample
{

private:

    CRITICAL_SECTION m_Crit1; // Protects variable a
    CRITICAL_SECTION m_Crit2; // Protects variable b
    CRITICAL_SECTION m_Crit3; // Protects variable c
    CRITICAL_SECTION m_Crit4; // Protects variable d

    // ...

};

Nun zu mir, dies scheint übertrieben. Ich dachte, kritische Abschnitte Fäden zwischen einem Prozess synchronisiert, so dass, wenn Sie eine haben können Sie es eingeben und keinen anderen Thread in diesem Prozess ausführen kann. Es besteht keine Notwendigkeit für einen kritischen Abschnitt für jede Variable, die Sie schützen möchten, wenn Sie in einem kritischen Abschnitt sind dann nichts anderes können Sie unterbrechen.

Ich denke, das einzige, was die Variablen von außerhalb eines kritischen Abschnitt ändern kann, ist, wenn der Prozess teilt sich eine Speicherseite mit einem anderen Prozess (können Sie das tun?) Und den anderen Prozess beginnt, die Werte zu ändern. Mutexes wäre auch hier, mit dem Namen mutexes wird geteilt über Prozesse helfen, oder nur Prozesse mit dem gleichen Namen?

FRAGE 3: Ist meine Analyse der kritischen Abschnitte zu korrigieren, und dieser Code sollte auf den Einsatz mutexes neu geschrieben werden? Ich habe einen Blick auf andere Synchronisierungsobjekte hatte (Semaphore und spinlocks), werden sie hier besser geeignet?

FRAGE 4: Wo sind kritische Abschnitte / mutexes / Semaphore / spinlocks am besten geeignet? Das heißt, das Synchronisationsproblem sollte sie angewendet werden. Gibt es eine große Leistungseinbuße für einen über die andere wählen?

Und während wir gerade dabei sind, ich gelesen, dass spinlocks sollte nicht in einer Single-Core-Multi-Thread-Umgebung verwendet wird, nur ein Multi-Core-multithreaded Umgebung. Also, FRAGE 5: Ist das falsch, oder wenn nicht, warum ist es richtig,

Vielen Dank im Voraus für alle Antworten:)

War es hilfreich?

Lösung

Q1: Mit dem "flüchtigen" keyword

  

In VS2005 und später der C ++ Compiler umgibt den Zugriff auf diese Variable Speicherbarrieren verwenden, um sicherzustellen, dass die Variable immer vollständig geschrieben / gelesen zu dem Hauptsystemspeicher, bevor es verwendet wird.

Genau. Wenn Sie nicht portablen Code erstellen, Visual Studio implementiert es genau auf diese Weise. Wenn Sie tragbar sein wollen, sind Ihre Optionen zur Zeit „begrenzt“. Bis C ++ 0x gibt es keine tragbare Art und Weise, wie atomare Operationen angeben, mit garantierter Lese- / Schreibreihenfolge und Sie müssen pro-Plattform-Lösungen implementieren. Das sei gesagt, boost bereits die schmutzige Arbeit für Sie getan haben, und Sie können verwenden sein Atom Primitive .

Q2: Variable Bedürfnisse sein 4-Byte / 8-Byte ausgerichtet

?

Wenn Sie sie halten ausgerichtet sind, sind Sie sicher. Wenn Sie dies nicht tun, werden die Regeln kompliziert (Cache-Zeilen, ...), daher der sicherste Weg, sie ausgerichtet zu halten, da dies leicht zu erreichen ist.

Q3: Should dieser Code zu verwenden mutexes neu geschrieben werden

?

Kritische Abschnitt ist ein leichtes Mutex. Es sei denn, Sie zwischen Prozessen synchronisieren müssen, verwenden Sie kritische Abschnitte.

Q4: Wo sind kritische Abschnitte / mutexes / Semaphore / spinlocks am besten geeignet

Kritische Abschnitte können sogar < a href = "http://msdn.microsoft.com/en-us/library/ms683476%28v=VS.85%29.aspx" rel = "nofollow noreferrer"> do Spin wartet für Sie.

Q5: Spinlocks sollte nicht in einem Single-Core verwendet werden

Spin Lock nutzt die Tatsache, dass während der Warte CPU dreht, eine andere CPU die Sperre wieder freigeben kann. Dies kann nicht nur mit einer CPU passieren, deshalb ist es nur eine Verschwendung von Zeit dort. Auf Multi-CPU kann Spinlocks gute Idee sein, aber es hängt davon ab, wie oft die Spin-Warte erfolgreich sein wird. Die Idee ist, für eine kurze Weile warten, viel schneller wird dann Kontextschalter dort zu tun und wieder zurück, also wenn die Warte es wahrscheinlich, kurz zu sein, ist es besser zu warten.

Andere Tipps

1) Kein flüchtige nur sagt wieder lädt den Wert aus dem Speicher jedes Mal ist es immer noch möglich, dass es die Hälfte aktualisiert werden.

Edit: 2) Windows bietet einige Atomfunktionen. Sehen Sie die „verzahnt“ Funktionen .

Die Kommentare führte mich tun ein bisschen mehr zu lesen auf. Wenn Sie durch die Intel-System Programming Guide Sie können sehen, dass es ausgerichtet lesen und Schreiben sind atomar.

8.1.1 atomare Operationen garantiert Der Intel486 Prozessor (und neuere Prozessoren da) garantieren, dass die folgenden Grundspeicheroperationen werden immer atomar ausgeführt werden:
• Lesen oder ein Byte
schreiben • Lesen oder ein Wort auf einem 16-Bit-Grenze
ausgerichtet Schreiben • Lesen oder Schreiben eines Doppelwort ausgerichtet auf einem 32-Bit-Grenze
Der Pentium-Prozessor (und neuere Prozessoren da) garantieren, dass die folgenden zusätzliche Speicheroperationen werden immer atomar ausgeführt werden:
• Lesen oder Schreiben einer Viererwortausgerichtet auf einem 64-Bit-Grenze
• 16-Bit-Zugriffe auf nicht gecachten Speicherstellen, die innerhalb eines 32-Bit-Datenbus
passen Die P6 Familie Prozessoren (und neuere Prozessoren da) garantieren, dass die folgenden zusätzliche Speicheroperation wird immer atomar ausgeführt werden:
• Unaligned 16-, 32-, und 64-Bit-Zugriffe auf den gecachten Speicher dass Passung in einem Cache- Linie
Zugriffe auf den cachefähigen Speicher, der über Busbreiten, Cache-Zeilen aufgeteilt sind, und Seitengrenzen sind nicht als Atom durch den Intel Core 2 Duo, Intel garantiert Atom, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, P6 Familie, Pentium und Intel486 Prozessoren. Der Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon und P6 Familie Prozessoren bieten Bus Steuersignale, erlauben externe Speichersubsysteme zu machen Split Zugriffe Atom; jedoch, nonaligned Datenzugriffe wird die Leistung des Prozessors ernsthaft beeinträchtigen und sollte vermieden werden. Ein x87-Befehl oder ein SSE-Befehle, die Daten größer als ein Quadwort zugreift kann unter Verwendung von mehreren Speicherzugriffe realisiert. Wenn ein solcher Befehl speichert in den Speicher, einige der Zugriffe vervollständigen (Schreiben in den Speicher), während ein anderer kann bewirkt, dass der Betrieb aus architektonischen Gründen (beispielsweise aufgrund eines Seitentabelleneintrags bemängeln Das ist mit „nicht vorhanden“). In diesem Fall werden die Effekte der abgeschlossenen Zugriffe kann Software sichtbar sein, auch wenn der Gesamt Befehl einen Fehler verursacht. wenn TLB Ungültigkeits wurde (siehe Abschnitt 4.10.3.4) verzögert, wie Seitenfehler auftreten können, wenn auch alle Zugriffe sind auf der gleichen Seite.

Also im Grunde ja, wenn Sie tun, eine 8-Bit-Lese- / Schreib von einem beliebigen Adresse eines 16-Bit-Lese / Schreibvorgang von einer 16-Bit-ausgerichteten Adresse etc etc Sie bekommen atomare Operationen. Es ist auch interessant, dass Sie nicht ausgerichteten Speicher tun können Lese- / Schreibvorgänge innerhalb einer Cache-Line auf einer modernen Maschine. Die Regeln scheinen recht komplex, obwohl so dass ich nicht auf sie verlassen würde, wenn ich du wäre. Prost auf die commenters, das ist eine gute Lernerfahrung für mich, dass man:)

3) Ein kritischer Abschnitt wird für seine Sperre ein paar Mal Spin-Lock versuchen, und sperrt dann einen Mutex. Spin-Locking kann die CPU-Leistung nichts zu tun saugen und ein Mutex eine Weile dauern kann seine Sachen zu tun. CriticalSections sind eine gute Wahl, wenn Sie nicht die verriegelte Funktionen verwenden können.

4) Es gibt Leistungseinbußen für eine über die andere Wahl. Es ist ein ziemlich großes fragen hier durch die Vorteile von allem zu gehen. Die MSDN-Hilfe hat viele gute Informationen über jede dieser. Ich sugegst sie zu lesen.

5) Sie können sie einen Spin-Lock in einer Single-Threaded-Umgebung normalerweise nicht notwendig, obwohl als Thread-Management-Mittel verwenden, dass Sie nicht mehr als 2 Prozessoren können gleichzeitig die gleichen Daten zugreifen. Es ist einfach nicht möglich.

1: Volatile an sich ist praktisch nutzlos für Multithreading. Es garantiert, dass die Lese- / Schreib wird ausgeführt werden, anstatt den Wert in einem Register zu speichern, und es ist sichergestellt, dass die Lese- / Schreib nicht neu geordnet werden in Bezug auf andere volatile liest / schreibt . Aber es kann immer noch in Bezug auf nichtflüchtigen diejenigen neu geordnet werden, die im Grunde 99,9% des Codes ist. Microsoft haben volatile neu definiert auch alle Zugriffe im Speicher Barrieren wickeln, aber das garantiert nicht der Fall ist in der Regel sein. Es wird nur brechen leise auf jedem Compiler, der volatile als Standard definiert hat. (Der Code wird kompiliert und ausgeführt, es wird nur nicht Thread-sicher mehr)

Abgesehen davon liest / schreibt auf ganzzahlige große Objekte sind atomar auf x86, solange das Objekt gut ausgerichtet. (Sie haben keine Garantie von , wenn die Schreib obwohl auftreten. Der Compiler und CPU kann es neu anordnen, so dass es atomar ist, aber nicht Thread-sicher)

. 2: Ja, das Objekt für den Lese- / Schreibausgerichtet sein Atom sein

3: Nicht wirklich. Nur ein Thread kann zu einem Zeitcode innerhalb eines bestimmten kritischen Abschnitts ausführen. Andere Threads können noch anderen Code auszuführen. So können Sie die jeweils vier Variablen geschützt durch einen anderen kritischen Abschnitt haben kann. Wenn sie alle den gleichen kritischen Abschnitt geteilt, würde ich nicht in der Lage sein Objekt zu manipulieren 1, während Sie Manipulieren Objekt 2, die ineffizient und Constraints Parallelität mehr als notwendig ist. Wenn sie durch verschiedene kritische Abschnitte geschützt sind, können wir einfach nicht beide manipulieren die gleichen Objekt gleichzeitig.

4: Spinlocks sind selten eine gute Idee. Sie sind nützlich, wenn Sie einen Thread erwarten nur warten zu müssen, eine sehr kurze Zeit, bevor der Lage, um die Sperre zu erwerben, und Sie absolut minimale Latenz neeed. Es vermeidet die OS Kontextschalter, der ein relativ langsamer Vorgang ist. Stattdessen sitzt der Faden nur in einer Schleife ständig Polling eine Variable. So höheren CPU-Auslastung (der Kern nicht freigegeben wird, bis einen anderen Thread ausgeführt werden, während für die spinlock warten), aber der Faden wird in der Lage sein, weiterhin , sobald als die Sperre aufgehoben wird.

Wie für die anderen, sind die Leistungsmerkmale so ziemlich das gleiche: nur verwenden je nachdem, was die Semantik besten hat für Ihre Bedürfnisse. Typischerweise kritische Abschnitte sind am bequemsten für gemeinsam benutzten Variablen zu schützen und mutexes leicht ein „Flag“ verwendet werden kann, so dass andere Threads zu setzen, um fortzufahren.

Was nicht spinlocks in einer Single-Core-Umgebung, denken Sie daran, dass die spinlock Ausbeute nicht wirklich. Gewinde A Warten auf eine spinlock nicht tatsächlich auf Eis ermöglicht das Betriebssystem Zeitplan Thread B zu laufen. Aber da A auf diesem spinlock warten, einiger anderer Thread ist zu haben, dass die Verriegelung zu lösen. Wenn Sie nur einen einzigen Kern haben, dann ist der andere Thread nur in der Lage sein zu laufen, wenn A abgeschaltet. Mit einem gesunden O, die früher oder später ohnehin als Teil des regulären Kontextwechsels passieren werden. Aber da wir wissen, dass A nicht in der Lage sein, um die Sperre zu bekommen, bis B eine Zeit ausgeführt und hatte Release die Sperre, würden wir besser dran, wenn A nur sofort ergab, setzen Sie wurde in eine Warteschlange von dem O, und neu gestartet, wenn B die Verriegelung gelöst hat. Und das ist, was alle andere Schlosstypen zu tun. Ein spinlock noch Arbeit in einer Single-Core-Umgebung (ein Betriebssystem mit präemptiven Multitasking vorausgesetzt), es wird nur sehr sehr ineffizient sein.

Sie keine flüchtigen verwenden. Es hat so gut wie nichts mit Thread-Sicherheit zu tun. Siehe hier für die low-down.

Die Zuordnung zu BOOL benötigt keine Synchronisation Primitiven. Es wird gut funktionieren ohne besonderen Aufwand auf Ihrer Seite.

Wenn Sie die Variable einstellen wollen, und dann stellen Sie sicher, dass ein anderer Thread den neuen Wert sieht, müssen Sie irgendeine Art von Kommunikation zwischen den beiden Threads etablieren. Sperren unmittelbar kurz vor vielleicht sind gekommen und gegangen bringt nichts, weil der andere Thread zuweisen, bevor Sie das Schloss erworben.

Ein letztes Wort der Vorsicht: Threading ist extrem hart richtig zu machen. Die erfahrenen Programmierer sind in der Regel die am wenigsten komfortabel mit der Verwendung von Fäden sein, die Glocken läuten Alarm für alle, die mit ihrer Verwendung sind unerfahren setzen soll. Ich empfehle, verwenden Sie einige höhere Ebene Primitiven Parallelität in Ihrer Anwendung zu implementieren. Vorbei an unveränderliche Datenstrukturen über Synchron Warteschlangen ist ein Ansatz, dass im Wesentlichen die Gefahr verringert wird.

Flüchtige Speicher nicht Barrieren bedeuten.

Es bedeutet nur, dass es ein Teil der empfundenen Zustand des Speichermodells sein. Die Folge davon ist, dass der Compiler die Variable nicht weg optimieren, noch können sie Operationen auf den Variable nur durchführen, in CPU-Register (es wird tatsächlich zu laden und zu speichern, um Speicher).

Da es keine Speicherbarrieren impliziert sind, kann der Compiler Befehle nach Belieben neu anordnen. Die einzige Garantie ist, dass die Reihenfolge, in der verschiedenen flüchtigen Variablen gelesen werden / Schreib wird wie im Code das gleiche sein:

void test() 
{
    volatile int a;
    volatile int b;
    int c;

    c = 1;
    a = 5;
    b = 3;
}

Mit dem obigen Code (das c unter der Annahme nicht optimiert entfernt) das Update auf c kann vor oder nach dem Updates a passieren und b und bietet drei mögliche Ergebnisse. Das a und b Updates ist garantiert, um durchgeführt werden. c kann von jedem Compiler leicht optimiert entfernt werden. Mit genügend Informationen, kann der Compiler optimieren sogar weg a und b (wenn nachgewiesen werden kann, dass keine anderen Threads die Variablen lesen und dass sie nicht auf eine Hardware-Array gebunden sind (so in diesem Fall können sie in der Tat entfernt werden). Beachten Sie, dass die Norm nicht ein bestimmtes Verhalten erfordert, sondern ein wahrnehmbarer Zustand mit der as-if Regel.

Fragen 3: CRITICAL_SECTIONs und Mutexes Arbeit, so ziemlich die gleiche Art und Weise. Ein Win32 Mutex ist ein Kernel-Objekt, so dass es zwischen Prozessen gemeinsam genutzt werden können, und wartete auf mit WaitForMultipleObjects, die Sie mit einem CRITICAL_SECTION nicht tun kann. Auf der anderen Seite, ist ein CRITICAL_SECTION leichtere und damit schneller. Aber die Logik des Codes sollten nicht betroffen sein, mit dem Sie verwenden.

Sie kommentierte auch, dass „es gibt keine Notwendigkeit für einen kritischen Abschnitt für jede Variable, die Sie schützen möchten, wenn Sie in einem kritischen Abschnitt dann nichts sind sonst können Sie unterbrechen.“ Das ist wahr, aber der Nachteil ist, dass Zugriffe auf eine der Variablen müssten Sie diese Sperre zu halten. Wenn die Variablen Bedeutung unabhängig aktualisiert werden können, verlieren Sie die Möglichkeit, für diese Operationen Parallelisierung. (Da es sich um Mitglieder des gleichen Objekts sind, obwohl, ich würde vor abschließendem Fest denkt, dass sie wirklich unabhängig voneinander zugänglich.)

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top