3 Stimmen

Zuweisung an flüchtige Variable in C#

Mein Verständnis von C# sagt (dank Jeff Richter & Jon Skeet), dass Zuweisung "atomar" ist. Was nicht ist, ist, wenn wir mischen liest & schreibt (Inkrement/Dekrement) und daher müssen wir Methoden auf die Interlocked verwenden. Wenn wir nur lesen und zuweisen, sind dann beide Operationen atomar?

`public class Xyz { private volatile int _lastValue; private IList<int> AvailableValues { get; set; } private object syncRoot = new object(); private Random random = new Random();

    //Accessible by multiple threads
    public int GetNextValue() //and return last value once store is exhausted
    {
        //...

        var count = 0;
        var returnValue = 0;

        lock (syncRoot)
        {
            count = AvailableValues.Count;
        }

        if (count == 0)
        {
            //Read... without locking... potential multiple reads
            returnValue = _lastValue;
        }
        else
        {

            var toReturn = random.Next(0, count);

            lock (syncRoot)
            {
                returnValue = AvailableValues[toReturn];
                AvailableValues.RemoveAt(toReturn);
            }
            //potential multiple writes... last writer wins
            _lastValue = returnValue;
         }

        return returnValue;

    }`

18voto

Eric Lippert Punkte 628543

Mein Verständnis von C# sagt (dank der Jeff Richter & Jon Skeet), dass Zuweisung "atomar" ist.

Die Zuweisung ist im Allgemeinen nicht atomar. In der C#-Spezifikation ist genau festgelegt, was garantiert atomar ist. Siehe Abschnitt 5.5:

Lese- und Schreibvorgänge der folgenden Datentypen sind atomar: bool, char, byte, sbyte, short, ushort, uint, int, float und Referenztypen. Darüber hinaus sind Lese- und Schreibvorgänge von Enum-Typen mit einem zugrundeliegenden Typ aus der vorherigen Liste ebenfalls atomar. Lese- und Schreibzugriffe auf andere Typen, einschließlich long, ulong, double und decimal, sowie auf benutzerdefinierte Typen, sind nicht garantiert atomar sein .

(Hervorhebung hinzugefügt.)

Wenn Sie nur Lesen & Zuweisen haben, würden beide Operationen atomar sein?

Auch hier gilt: Abschnitt 5.5 beantwortet Ihre Frage:

es gibt keine Garantie für atomares Lesen-Ändern-Schreiben

11voto

Marc Gravell Punkte 970173

volatile ist eigentlich mehr mit der Zwischenspeicherung (in Registern usw.) verbunden; mit volatile Sie wissen, dass dieser Wert tatsächlich in den Speicher geschrieben/aus dem Speicher gelesen wird sofort (was im Übrigen nicht immer der Fall ist). Dadurch können verschiedene Threads sofort die Aktualisierungen der anderen sehen. Es gibt noch andere subtile Probleme mit der Umordnung von Anweisungen, aber das wird komplex.

Hier sind zwei Bedeutungen von "atomar" zu berücksichtigen:

  • ist ein einzelner Lesevorgang für sich selbst atomar / ein einzelner Schreibvorgang für sich selbst atomar (d.h. könnte ein anderer Thread zwei verschiedene Hälften von zwei Double s, was zu einer Zahl führt, die nie existierte)
  • ist ein Lese-/Schreibpaar atomar/isoliert zusammen

Das "an sich" hängt von der Größe des Wertes ab; kann er in einer einzigen Operation aktualisiert werden? Das Lese-/Schreibpaar hat eher mit der Isolierung zu tun, d. h. mit der Vermeidung verlorener Aktualisierungen.

In Ihrem Beispiel ist es möglich, dass zwei Threads die gleiche _lastValue führen beide die Berechnungen durch und aktualisieren dann (getrennt) _lastValue . Eine dieser Aktualisierungen wird verloren gehen. In Wirklichkeit wollen Sie wohl eine lock über die Dauer des Lese-/Schreibvorgangs.

5voto

Matt Howells Punkte 38730

Die Verwendung des Schlüsselworts volatile macht den Zugriff nicht thread-sicher, sondern stellt nur sicher, dass Lesevorgänge der Variablen aus dem Speicher gelesen werden, anstatt sie möglicherweise aus einem Register zu lesen, in dem sie von einem früheren Lesevorgang zwischengespeichert sind. Bei bestimmten Architekturen wird diese Optimierung vorgenommen, was dazu führen kann, dass veraltete Werte verwendet werden, wenn mehrere Threads auf dieselbe Variable schreiben.

Um den Zugriff richtig zu synchronisieren, benötigen Sie eine breitere Sperre:

public class Xyz
{
    private volatile int _lastValue;
    private IList<int> AvailableValues { get; set; }
    private object syncRoot = new object();
    private Random rand = new Random();

    //Accessible by multiple threads
    public int GetNextValue() //and return last value once store is exhausted
    {
        //...

        lock (syncRoot)
        {
            var count = AvailableValues.Count;
            if(count == 0)
                return _lastValue;

            toReturn = rand.Next(0, count);
            _lastValue = AvailableValues[toReturn];
            AvailableValues.RemoveAt(toReturn);
        }
        return _lastValue;
    }
}

Wenn die Leistung eine Rolle spielt, sollten Sie die Verwendung einer LinkedList für AvailableValues in Betracht ziehen, da diese eine O(1)-Entfernungsoperation unterstützt.

2voto

Steve Cooper Punkte 18836

Für .Net 2.0 und früher gibt es eine Klasse namens ReaderWriterLock die es Ihnen ermöglicht, Schreib- und Lesevorgänge getrennt zu blockieren. Könnte hilfreich sein.

Für .Net 3.5 und höher sollten Sie ReaderWriterLockSlim die Microsoft folgendermaßen beschreibt;

ReaderWriterLockSlim ist ähnlich wie ReaderWriterLock, hat aber vereinfachte Regeln für die Rekursion und für das Hoch- und Herunterstufen des Sperrstatus. ReaderWriterLockSlim vermeidet viele Fälle von potenziellen Deadlocks. Darüber hinaus ist die Leistung von ReaderWriterLockSlim deutlich besser als die von ReaderWriterLock. ReaderWriterLockSlim wird für alle Neuentwicklungen empfohlen.

0voto

nothrow Punkte 15286

Dies (Atomarität) ist NICHT garantiert.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X