2009-02-27 12 views
17

Ich lese in der MS-Dokumentation, dass die Zuweisung eines 64-Bit-Wert auf einem 32-Bit-Intel-Computer keine atomare Operation ist; Das heißt, die Operation ist nicht Thread-sicher. Das bedeutet, wenn zwei Personen gleichzeitig einem statischen Int64 Feld einen Wert zuweisen, kann der endgültige Wert des Feldes nicht vorhergesagt werden.Unter C# ist Int64 auf einem 32-Bit-Prozessor gefährlich

Drei Frage Teil:

  • Ist das wirklich wahr?
  • Ist das etwas, um das ich mich in der realen Welt sorgen würde?
  • Wenn meine Anwendung Multithreading ist, muss ich wirklich alle meine Int64 Zuordnungen mit Sperrcode umgeben?
+3

Für atomare Operationen auf Int64 können Sie die InterLocked-Klasse (http://msdn.microsoft.com/en-us/library/system.threading.interlocked.add.aspx) verwenden. –

Antwort

18

Es geht nicht um jede Variable, auf die Sie stoßen. Wenn eine Variable als gemeinsamer Status oder etwas verwendet wird (einschließlich, aber nicht beschränkt auf einigestatic Felder), sollten Sie dieses Problem beheben. Für lokale Variablen, die nicht als Konsequenz der Schließung in einer Closure- oder Iterator-Transformation gehostet werden und von einer einzelnen Funktion (und somit einem einzelnen Thread) gleichzeitig verwendet werden, ist dies völlig ausgeschlossen.

+0

Das ist richtig, aber es ist vielleicht nicht klar warum. Ein Int64 wird von System geerbt.ValueType, was bedeutet, dass der Wert auf dem Stack gespeichert wird. Da jeder Thread seinen eigenen Aufruf-Stack erhält, hat jeder Thread seinen eigenen Wert, selbst wenn er die gleiche Funktion aufruft. – codekaizen

+0

vorstellen Klasse X {int n; }. Ist es Referenz oder Werttyp? Wird es im Heap oder im Stack gespeichert? –

+0

DK, ich glaube nicht, dass dies eine relevante Frage ist, aber Klassen sind Referenztypen und werden in einem Heap gespeichert. Wenn Sie einen Verweis auf eine Klasse in nur einem einzigen Thread speichern, müssen Sie sich keine Sorgen um Sperrprobleme machen. –

7

MSDN:

eine Instanz dieses Typs Zuweisen nicht Plattformen auf allen Hardware-Thread sicher, weil der binäre Darstellung dieser Instanz zu groß sein könnte, in einer einzigen atomaren Operation zuzuweisen.

Aber auch:

Wie bei jeder anderen Art, Lesen und auf eine gemeinsame Variable zu schreiben, dass enthält eine Instanz dieses Typs muss durch eine Sperre geschützt werden Thread-Sicherheit zu gewährleisten.

+2

True, das Schlüsselwort ist ** shared variable **. –

1

Auf einer 32-Bit-x86-Plattform ist das größte Speicherelement mit atomarer Größe 32-Bit.

Dies bedeutet, dass beim Lesen oder Schreiben von Daten aus einer 64-Bit-Variablen Lese-/Schreibzugriff während der Ausführung möglich ist.

  • Beispielsweise beginnen Sie, einer 64-Bit-Variablen einen Wert zuzuweisen.
  • Nachdem die ersten 32 Bits geschrieben wurden, entscheidet das Betriebssystem, dass ein anderer Prozess CPU-Zeit erhalten wird.
  • Der nächste Prozess versucht, die Variable zu lesen, der Sie gerade zugewiesen haben.

Das ist nur eine mögliche Race-Bedingung mit 64-Bit-Zuweisung auf einer 32-Bit-Plattform.

Allerdings kann es auch bei 32-Bit-Variablen zu Race-Bedingungen mit Lesen und Schreiben kommen. Daher sollte jede gemeinsam genutzte Variable auf irgendeine Weise synchronisiert werden, um diese Race-Bedingungen zu lösen.

+0

"Auf einer 32-Bit-x86-Plattform ist das größte Speicherelement mit Atomgröße 32-Bit." - Das ist falsch. Sie können 8 Byte atomar über fstp/mmx/sse schreiben. –

0

Ist das wirklich wahr?Ja, wie sich herausstellt. Wenn Ihre Register nur 32 Bits enthalten und Sie an einem Speicherort einen 64-Bit-Wert speichern müssen, werden zwei Ladeoperationen und zwei Speicheroperationen benötigt. Wenn Ihr Prozess durch einen anderen Prozess zwischen diesen beiden Loads/Stores unterbrochen wird, kann der andere Prozess die Hälfte Ihrer Daten beschädigen! Komisch aber wahr. Dies ist bei jedem Prozessor, der jemals gebaut wurde, ein Problem. Wenn Ihr Datentyp länger ist als Ihre Register, haben Sie Probleme mit der Nebenläufigkeit.

Ist das etwas, worüber ich mich in der realen Welt sorgen würde? Ja und nein. Da fast alle modernen Programmiersprachen einen eigenen Adressraum haben, müssen Sie sich nur darüber Gedanken machen, wenn Sie Multi-Threaded programmieren.

Wenn meine Anwendung Multithreading ist, muss ich wirklich alle meine Int64-Zuweisungen mit Sperrcode umgeben? Leider, ja, wenn Sie technisch werden wollen. In der Praxis ist es in der Praxis einfacher, einen Mutex oder einen Semaphor um größere Codebausteine ​​zu verwenden, als jede einzelne set-Anweisung für global zugängliche Variablen zu sperren.

2

Wenn Sie eine gemeinsam genutzte Variable haben (z. B. als statisches Feld einer Klasse oder als Feld eines gemeinsamen Objekts) und dieses Feld oder Objekt kreuzweise verwendet wird, dann ja Sie müssen sicherstellen, dass der Zugriff auf diese Variable über eine atomare Operation geschützt ist. Der x86-Prozessor verfügt über Intrinsics, um sicherzustellen, dass dies geschieht, und diese Funktion wird durch die System.Threading.Interlocked-Klassenmethoden verfügbar gemacht.

Zum Beispiel:

class Program 
{ 
    public static Int64 UnsafeSharedData; 
    public static Int64 SafeSharedData; 

    static void Main(string[] args) 
    { 
     Action<Int32> unsafeAdd = i => { UnsafeSharedData += i; }; 
     Action<Int32> unsafeSubtract = i => { UnsafeSharedData -= i; }; 
     Action<Int32> safeAdd = i => Interlocked.Add(ref SafeSharedData, i); 
     Action<Int32> safeSubtract = i => Interlocked.Add(ref SafeSharedData, -i); 

     WaitHandle[] waitHandles = new[] { new ManualResetEvent(false), 
              new ManualResetEvent(false), 
              new ManualResetEvent(false), 
              new ManualResetEvent(false)}; 

     Action<Action<Int32>, Object> compute = (a, e) => 
              { 
               for (Int32 i = 1; i <= 1000000; i++) 
               { 
                a(i); 
                Thread.Sleep(0); 
               } 

               ((ManualResetEvent) e).Set(); 
              }; 

     ThreadPool.QueueUserWorkItem(o => compute(unsafeAdd, o), waitHandles[0]); 
     ThreadPool.QueueUserWorkItem(o => compute(unsafeSubtract, o), waitHandles[1]); 
     ThreadPool.QueueUserWorkItem(o => compute(safeAdd, o), waitHandles[2]); 
     ThreadPool.QueueUserWorkItem(o => compute(safeSubtract, o), waitHandles[3]); 

     WaitHandle.WaitAll(waitHandles); 
     Debug.WriteLine("Unsafe: " + UnsafeSharedData); 
     Debug.WriteLine("Safe: " + SafeSharedData); 
    } 
} 

Die Ergebnisse:

Unsafe: -24050275641 Sicher: 0

Auf interessante Randnotiz, lief ich diese im x64-Modus auf Vista 64. Dies zeigt, dass 64-Bit-Felder werden von der Laufzeit wie 32-Bit-Felder behandelt, dh 64-Bit-Operationen sind nicht atomar. Wer weiß, ob dies ein CLR-Problem oder ein x64-Problem ist?

+0

Wie Jon Skeet und Ben S darauf hingewiesen haben, könnte die Race-Bedingung zwischen Lese- und Schreibvorgängen auftreten, so dass man nicht schlussfolgern kann, dass die Schreibvorgänge nicht atomar waren. –

+0

Ich verstehe nicht ... dieses Argument geht in beide Richtungen. Soweit ich das beurteilen kann, sind die Daten immer noch falsch. Wenn Sie das Beispiel ausführen, ist es offensichtlich, dass die Daten aufgrund nicht atomarer Operationen falsch sind. – codekaizen

+0

Das Problem, weder CLR noch x64. Es ist mit deinem Code. Was du versuchst zu tun, ist atomares Lesen + Addieren/Subtrahieren + Schreiben. Während in x64 garantiert atomare lesen/schreiben von int64. Auch dies unterscheidet sich von atomischem Lesen + Hinzufügen + Schreiben. –

12

Auch wenn die schreibt war atomare, Chancen, Sie würden immer noch eine Sperre herausnehmen müssen, wenn Sie auf die Variable zugegriffen haben. Wenn Sie das nicht tun würden, müssten Sie mindestens die Variable volatile erstellen, um sicherzustellen, dass alle Threads den nächsten Wert beim nächsten Lesen der Variablen sehen (was fast immer das ist, was Sie wollen). Dadurch können Sie atomare, volatile Mengen erstellen - aber sobald Sie etwas Interessanteres tun möchten, z. B. das Hinzufügen von 5, würden Sie wieder sperren.

Lock freie Programmierung ist sehr, sehr schwer, richtig zu bekommen. Sie müssen wissen, genau was Sie tun, und halten Sie die Komplexität so klein wie ein Stück Code wie möglich. Persönlich versuche ich selten, es anders als für sehr gut bekannte Muster zu versuchen, wie zum Beispiel einen statischen Initialisierer zu verwenden, um eine Sammlung zu initialisieren und dann ohne Sperren aus der Sammlung zu lesen.

Die Verwendung der Interlocked Klasse kann in einigen Situationen helfen, aber es ist fast immer viel einfacher, nur eine Sperre herauszunehmen. Unangefochtene Sperren sind "ziemlich billig" (zugegebenermaßen werden sie mit mehr Kernen teuer, aber das gilt auch für alles). Machen Sie sich nicht mit Lock-Free-Code herum, bis Sie einen guten Beweis dafür haben, dass es tatsächlich einen signifikanten Unterschied macht.