2017-02-09 5 views
4

Wir haben eine Methode, die den globalen Sequenzindex aller Ereignisse in unserer Anwendung verwaltet. Wie es die Website ist, wird erwartet, dass diese Methode threadsicher ist. Thread-sichere Implementierung wurde nach:Interlocked.Increment und Rückgabe des inkrementierten Werts

private static long lastUsedIndex = -1; 

public static long GetNextIndex() 
{ 
    Interlocked.Increment(ref lastUsedIndex); 
    return lastUsedIndex; 
} 

Allerdings bemerkten wir, dass unter gewissen nicht schwere Last dupliziert Indizes in System erschien. Ein einfacher Test zeigte, dass es etwa 1500 Duplikate für 100000 Iterationen gibt.

public static long GetNextIndex() 
{ 
    return Interlocked.Increment(ref lastUsedIndex); 
} 

Allerdings verstehe ich nicht klar, warum erste Implementierung funktionierte nicht:

internal class Program 
{ 
    private static void Main(string[] args) 
    { 
     TestInterlockedIncrement.Run(); 
    } 
} 

internal class TestInterlockedIncrement 
{ 
    private static long lastUsedIndex = -1; 

    public static long GetNextIndex() 
    { 
     Interlocked.Increment(ref lastUsedIndex); 
     return lastUsedIndex; 
    } 

    public static void Run() 
    { 
     var indexes = Enumerable 
      .Range(0, 100000) 
      .AsParallel() 
      .WithDegreeOfParallelism(32) 
      .WithExecutionMode(ParallelExecutionMode.ForceParallelism) 
      .Select(_ => GetNextIndex()) 
      .ToList(); 

     Console.WriteLine($"Total values: {indexes.Count}"); 
     Console.WriteLine($"Duplicate values: {indexes.GroupBy(i => i).Count(g => g.Count() > 1)}"); 
    } 
} 

Dies kann mit folgenden Umsetzung festgelegt werden. Kann mir jemand helfen zu beschreiben, was in diesem Fall passiert?

+1

Da 'lastUsedIndex' durch einen weiteren Aufruf von' Interlocked.Increment' zwischen dem Abrufen des Ergebnisses und dem Zurücksenden an den Aufrufer aktualisiert werden könnte. –

+4

Standard Threading Race Bug, ein anderer Thread kann die Variable auch vor 'return lastUsedIndex' inkrementieren: 'Kleine Chancen, nicht Null. Der Code liest '[read modify write] read. Die Atomicity-Garantie, die Sie von Interlocked erhalten, gilt nur für die Operationen in den Klammern. Sie müssten 'lock [read modify write read]' verwenden, um es sicher zu machen. –

+0

RB, Hans Passant, verstanden. Kann jemand von euch es bitte als Antwort schreiben, damit es vollständig beantwortet wird? –

Antwort

2

Wenn es die Art und Weise in Ihrem ursprünglichen Beispiel gearbeitet, könnte man auch sagen, dass es für einen allgemeinen Fall von

Interlocked.Increment(ref someValue); 

// Any number of operations 

return someValue; 

funktionieren würde für diese wahr zu sein, würden Sie alle Gleichzeitigkeit beseitigen müssen (einschließlich Parallelität, Reëntrancy, Präemptive Codeausführung ...) zwischen Increment und der Rückgabe. Schlimmer noch, Sie müssen sicherstellen, dass, selbst wenn someValue zwischen der Rückkehr und der Increment verwendet wird, es in keiner Weise die Rückkehr beeinflussen würde. Mit anderen Worten - someValue muss unmöglich sein (unveränderlich) zwischen den beiden Aussagen zu ändern.

Sie können klar sehen, dass, wenn dies der Fall wäre, Sie nicht Interlocked.Increment an erster Stelle benötigen - Sie würden nur someValue++ tun. Der ganze Zweck von Interlocked und anderen atomaren Operationen besteht darin, sicherzustellen, dass die Operation entweder sofort (atomar) oder gar nicht stattfindet. Insbesondere schützt es Sie vor jeder Art von Neuordnung von Befehlen (entweder durch CPU-Optimierungen oder durch mehrere Threads, die parallel auf zwei logischen CPUs ausgeführt werden oder auf einer einzelnen CPU vorgehalten werden). Aber nur innerhalb der atomaren Operation. Das folgende Lesen von someValue ist nicht Teil der gleichen atomaren Operation (es ist Atom selbst, aber zwei atomare Operationen machen nicht die Summe auch Atom).

Aber Sie versuchen nicht, "eine beliebige Anzahl von Operationen" zu tun, oder? Eigentlich bist du es.Da andere Threads in Bezug auf Ihren Thread asynchron ausgeführt werden, wird Ihr Thread möglicherweise von einem dieser Threads vorbelegt, oder die Threads werden möglicherweise tatsächlich parallel auf mehreren logischen CPUs ausgeführt.

In einer realen Umgebung, bietet Ihr Beispiel eine ständig wachsende Feld (so es ein bisschen besser als someValue++ ist), aber es funktioniert nicht mit eindeutigen IDs zur Verfügung stellen, weil alles, was Sie lesen sind, ist someValue bei ein unsicherer Moment in der Zeit. Wenn zwei Threads versuchen, das Inkrement gleichzeitig auszuführen, werden beide erfolgreich sein (Interlocked.Incrementist Atom), aber sie werden auch den gleichen Wert von someValue lesen.

Das bedeutet nicht, dass Sie immer den Rückgabewert Interlocked.Increment verwenden möchten - wenn Sie mehr an dem Inkrement selbst interessiert sind, und nicht an dem erhöhten Wert. Ein typisches Beispiel könnte eine billige Profilerstellungsmethode sein - jeder Methodenaufruf könnte ein geteiltes Feld inkrementieren, und dann wird der Wert einmal für z. ein Durchschnitt von Anrufen pro Sekunde.

2

Nach den Kommentaren passiert folgendes.

Angenommen, wir haben lastUsedIndex == 5 und 2 parallele Threads.

Der erste Thread wird ausgeführt Interlocked.Increment(ref lastUsedIndex); und lastUsedIndex wird 6 werden. Dann wird der zweite Thread Interlocked.Increment(ref lastUsedIndex); ausführen und lastUsedIndex wird 7 werden.

Dann geben beide Threads den Wert lastUsedIndex zurück (denken Sie daran, dass sie parallel sind). Und dieser Wert ist jetzt 7.

In der zweiten Implementierung werden beide Threads Ergebnis der Interlocked.Increment() Funktion zurückgeben. Was in jedem Thread anders sein wird (6 und 7). Mit anderen Worten, in der zweiten Implementierung geben wir eine Kopie des inkrementierten Wertes zurück, und diese Kopie ist in anderen Threads nicht betroffen.

+0

Beachten Sie, dass das gleiche theoretisch auch auf einer Single-CPU-Maschine passieren könnte (wo die zwei Threads nicht gleichzeitig laufen würden), obwohl es natürlich noch weniger wahrscheinlich wäre (und je nachdem, was CPU, Compiler, Laufzeit und Betriebssystem macht, vielleicht unmöglich, aber nicht zuverlässig so). Der erste Thread * könnte * nach dem 'Increment', aber vor dem nachfolgenden Lesen von 'someValue' vorgelesen werden. – Luaan

Verwandte Themen