Wie teuer ist die Sperranweisung?

111

Ich habe mit Multithreading und Parallelverarbeitung experimentiert und brauchte einen Zähler, um eine grundlegende Zählung und statistische Analyse der Verarbeitungsgeschwindigkeit durchzuführen. Um Probleme bei der gleichzeitigen Verwendung meiner Klasse zu vermeiden, habe ich eine Sperranweisung für eine private Variable in meiner Klasse verwendet:

private object mutex = new object();

public void Count(int amount)
{
 lock(mutex)
 {
  done += amount;
 }
}

Aber ich habe mich gefragt ... wie teuer ist das Sperren einer Variablen? Was sind die negativen Auswirkungen auf die Leistung?

Kees C. Bakker
quelle
10
Das Sperren der Variablen ist nicht so teuer. Es ist das Warten auf eine gesperrte Variable, das Sie vermeiden möchten.
Gabe
53
Es ist viel billiger als Stunden damit zu verbringen, eine andere Rennbedingung
aufzuspüren ;-)
2
Nun ... wenn eine Sperre teuer ist, können Sie sie vermeiden, indem Sie die Programmierung so ändern, dass weniger Sperren benötigt werden. Ich könnte eine Art Synchronisation implementieren.
Kees C. Bakker
1
Ich hatte eine dramatische Verbesserung der Leistung (gerade jetzt, nachdem ich den Kommentar von @Gabe gelesen hatte), indem ich einfach viel Code aus meinen Sperrblöcken verschoben habe. Fazit: Von nun an lasse ich nur noch den variablen Zugriff (normalerweise eine Zeile) in einem Sperrblock, eine Art "Just-in-Time-Sperrung". Macht das Sinn?
Heltonbiker
2
@heltonbiker Natürlich macht es Sinn. Es sollte auch ein architektonisches Prinzip sein, man soll Schlösser so kurz, einfach und schnell wie möglich machen. Nur wirklich notwendige Daten, die synchronisiert werden müssen. Bei Serverboxen sollten Sie auch die hybride Natur des Schlosses berücksichtigen. Konflikte, auch wenn sie für Ihren Code nicht kritisch sind, sind auf die hybride Natur der Sperre zurückzuführen, die dazu führt, dass sich Kerne bei jedem Zugriff drehen, wenn die Sperre von einer anderen Person gehalten wird. Sie verschlingen effektiv einige CPU-Ressourcen von anderen Diensten auf dem Server für einige Zeit, bevor Ihr Thread angehalten wird.
Ipavlu

Antworten:

86

Hier ist ein Artikel , der in die Kosten geht. Kurze Antwort ist 50ns.

Jake Pearson
quelle
39
Kurze bessere Antwort: 50 ns + Wartezeit, wenn ein anderer Thread die Sperre hält.
Herman
4
Je mehr Threads in die Sperre eintreten und diese verlassen, desto teurer wird es. Die Kosten
steigen
16
Einige Zusammenhänge: Das Teilen von zwei Zahlen auf einem 3-GHz-x86-Gerät dauert ungefähr 10 ns (ohne die Zeit, die zum Abrufen / Dekodieren des Befehls benötigt wird) ; und das Laden einer einzelnen Variablen aus dem (nicht zwischengespeicherten) Speicher in ein Register dauert ungefähr 40 ns. So 50ns ist irrsinnig, blendend schnell - Sie sollten sich keine Sorgen über die Kosten der Verwendung von lockmehr als Sie über die Kosten der Verwendung einer variablen sorgen würde.
BlueRaja - Danny Pflughoeft
3
Außerdem war dieser Artikel alt, als diese Frage gestellt wurde.
Otis
3
Wirklich tolle Metrik, "fast keine Kosten", ganz zu schweigen von falsch. Ihr berücksichtigt nicht, dass es nur kurz und schnell ist und NUR, wenn es überhaupt keinen Streit gibt, einen Thread. In diesem Fall brauchen Sie überhaupt kein Schloss. Das zweite Problem: Sperre ist keine Sperre, sondern eine Hybridsperre. Sie erkennt innerhalb der CLR, dass die Sperre aufgrund atomarer Operationen von niemandem gehalten wird, und vermeidet in diesem Fall Aufrufe des Betriebssystemkerns, dh eines anderen Rings, der von diesen nicht gemessen wird Tests. Was als 25ns bis 50ns gemessen wird, ist tatsächlich auf Anwendungsebene verriegelter Anweisungscode, wenn die Sperre nicht genommen wird
ipavlu
50

Die technische Antwort lautet, dass dies nicht quantifizierbar ist. Dies hängt stark vom Status der Rückschreibpuffer des CPU-Speichers ab und davon, wie viele Daten, die der Prefetcher gesammelt hat, verworfen und erneut gelesen werden müssen. Welche sind beide sehr nicht deterministisch. Ich verwende 150 CPU-Zyklen als Back-of-the-Envelope-Näherung, um größere Enttäuschungen zu vermeiden.

Die praktische Antwort ist, dass es viel billiger ist als die Zeit, die Sie beim Debuggen Ihres Codes benötigen, wenn Sie glauben, eine Sperre überspringen zu können.

Um eine harte Zahl zu erhalten, müssen Sie messen. Visual Studio verfügt über einen Slick Concurrency Analyzer als Erweiterung.

Hans Passant
quelle
1
Eigentlich nein, es kann quantifiziert und gemessen werden. Es ist einfach nicht so einfach, wie diese Sperren rund um den Code zu schreiben und dann zu sagen, dass es nur 50 ns sind, ein Mythos, der am Single-Thread-Zugriff auf die Sperre gemessen wird.
Ipavlu
8
" Ich denke, du kannst ein Schloss überspringen" ... Ich denke, dort sind viele Leute, wenn sie diese Frage lesen ...
Snoop
30

Weiterführende Literatur:

Ich möchte einige meiner Artikel vorstellen, die sich für allgemeine Synchronisationsprimitive interessieren und sich mit Monitor, Verhalten, Eigenschaften und Kosten von C # -Sperranweisungen befassen, abhängig von bestimmten Szenarien und der Anzahl der Threads. Es ist speziell an CPU-Verschwendung und Durchsatzzeiten interessiert, um zu verstehen, wie viel Arbeit in mehreren Szenarien durchgesetzt werden kann:

https://www.codeproject.com/Articles/1236238/Unified-Concurrency-I-Introduction https://www.codeproject.com/Articles/1237518/Unified-Concurrency-II-benchmarking-methodologies https: // www. codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking

Ursprüngliche Antwort:

Ach je!

Es scheint, dass die richtige Antwort, die hier als DIE ANTWORT gekennzeichnet ist, von Natur aus falsch ist! Ich möchte den Autor der Antwort respektvoll bitten, den verlinkten Artikel bis zum Ende zu lesen. Artikel

Der Autor des Artikels von 2003 Artikeln wurde die Messung auf Dual - Core - Maschine nur und in dem ersten Mess Fall er gemessen mit einer einzigen Gewindesicherung nur und das Ergebnis war etwa 50 ns pro Schloss Zugang.

Es sagt nichts über eine Sperre in der gleichzeitigen Umgebung aus. Wir müssen also den Artikel weiter lesen und in der zweiten Hälfte hat der Autor das Sperrszenario mit zwei und drei Threads gemessen, was den Parallelitätsstufen der heutigen Prozessoren näher kommt.

Der Autor sagt also, dass bei zwei Threads auf Dual Core die Sperren 120 ns kosten und bei drei Threads 180 ns. Es scheint also eindeutig von der Anzahl der Threads abhängig zu sein, die gleichzeitig auf die Sperre zugreifen.

Es ist also einfach, es sind keine 50 ns, es sei denn, es ist ein einzelner Thread, bei dem die Sperre unbrauchbar wird.

Ein weiteres zu berücksichtigendes Problem ist, dass es als durchschnittliche Zeit gemessen wird !

Wenn die Zeit der Iterationen gemessen würde, gäbe es sogar Zeiten zwischen 1 ms und 20 ms, einfach weil die Mehrheit schnell war, aber nur wenige Threads auf die Prozessorzeit warten und sogar Millisekunden lange Verzögerungen verursachen.

Dies sind schlechte Nachrichten für jede Art von Anwendung, die einen hohen Durchsatz und eine geringe Latenz erfordert.

Und das letzte zu berücksichtigende Problem ist, dass es innerhalb des Schlosses zu langsameren Vorgängen kommen kann, und dies ist sehr oft der Fall. Je länger der Codeblock innerhalb des Schlosses ausgeführt wird, desto höher ist die Konkurrenz und die Verzögerungen steigen himmelhoch.

Bitte beachten Sie, dass bereits seit 2003 mehr als ein Jahrzehnt vergangen ist, dh nur wenige Generationen von Prozessoren, die speziell für den vollständigen gleichzeitigen Betrieb entwickelt wurden und das Sperren ihre Leistung erheblich beeinträchtigt.

ipavlu
quelle
1
Zur Verdeutlichung heißt es in dem Artikel nicht, dass sich die Sperrleistung mit der Anzahl der Threads in der Anwendung verschlechtert. Die Leistung nimmt mit der Anzahl der Threads ab, die über die Sperre streiten. (Dies ist in der obigen Antwort impliziert, aber nicht klar angegeben.)
Stachelbeere
Ich nehme an, Sie meinen das so: "Es scheint also eindeutig von der Anzahl der Threads abhängig zu sein, auf die gleichzeitig zugegriffen wird, und mehr ist schlimmer." Ja, der Wortlaut könnte besser sein. Ich meinte "gleichzeitiger Zugriff" als Threads, die gleichzeitig auf die Sperre zugreifen und so Konflikte erzeugen.
Ipavlu
20

Dies beantwortet Ihre Frage zur Leistung nicht, aber ich kann sagen, dass .NET Framework eine Interlocked.AddMethode bietet , mit der Sie Ihre amountzu Ihrem doneMitglied hinzufügen können , ohne ein anderes Objekt manuell zu sperren.

Adam Maras
quelle
1
Ja, das ist wahrscheinlich die beste Antwort. Aber hauptsächlich wegen des kürzeren und saubereren Codes. Der Geschwindigkeitsunterschied ist wahrscheinlich nicht spürbar.
Henk Holterman
Danke für diese Antwort. Ich mache mehr Sachen mit Schlössern. Hinzugefügte Ints sind eine von vielen. Ich liebe den Vorschlag, werde ihn von nun an verwenden.
Kees C. Bakker
Sperren sind viel, viel einfacher zu korrigieren, selbst wenn der Code ohne Sperren möglicherweise schneller ist. Interlocked.Add alleine hat die gleichen Probleme wie + = ohne Synchronisation.
Hangar
10

lock (Monitor.Enter / Exit) ist sehr billig, billiger als Alternativen wie Waithandle oder Mutex.

Aber was wäre, wenn es (ein wenig) langsam wäre, hätten Sie lieber ein schnelles Programm mit falschen Ergebnissen?

Henk Holterman
quelle
5
Haha ... ich wollte das schnelle Programm und die guten Ergebnisse.
Kees C. Bakker
@ henk-holterman Es gibt mehrere Probleme mit Ihren Aussagen: Erstens, wie diese Frage und die Antworten deutlich gezeigt haben, gibt es ein geringes Verständnis für die Auswirkungen der Sperre auf die Gesamtleistung, selbst wenn Leute einen Mythos über 50 ns angeben, der nur in einer Umgebung mit einem Thread anwendbar ist. Zweitens ist Ihre Aussage hier und wird jahrelang und in der Zwischenzeit Prozessoren bleiben, die in Kernen wachsen, aber die Geschwindigkeit der Kerne ist nicht so hoch. ** Thrid ** -Anwendungen werden mit der Zeit nur komplexer, und dann ist es Schicht für Schicht Locking in der Umgebung vieler Kerne und die Zahl steigt, 2,4,8,10,20,16,32
ipavlu
Mein üblicher Ansatz ist es, die Synchronisation lose gekoppelt mit so wenig Interaktion wie möglich aufzubauen. Das geht sehr schnell zu sperrenfreien Datenstrukturen. Ich habe meine Code-Wrapper für Spinlock erstellt, um die Entwicklung zu vereinfachen, und selbst wenn TPL über spezielle gleichzeitige Sammlungen verfügt, habe ich eigene Spin-Lock-Sammlungen für Liste, Array, Wörterbuch und Warteschlange entwickelt, da ich wenig mehr Kontrolle und manchmal etwas Code benötigte, der unter ausgeführt wird Spinlock. Ich kann Ihnen sagen, es ist möglich und ermöglicht es, mehrere Szenarien zu lösen, die TPL-Sammlungen nicht können und mit großem Leistungs- / Durchsatzgewinn.
Ipavlu
7

Die Kosten für ein Schloss in einer engen Schleife sind im Vergleich zu einer Alternative ohne Schloss enorm. Sie können es sich leisten, viele Schleifen zu erstellen und trotzdem effizienter als ein Schloss zu sein. Deshalb sind sperrfreie Warteschlangen so effizient.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LockPerformanceConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            const int LoopCount = (int) (100 * 1e6);
            int counter = 0;

            for (int repetition = 0; repetition < 5; repetition++)
            {
                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    lock (stopwatch)
                        counter = i;
                stopwatch.Stop();
                Console.WriteLine("With lock: {0}", stopwatch.ElapsedMilliseconds);

                stopwatch.Reset();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++)
                    counter = i;
                stopwatch.Stop();
                Console.WriteLine("Without lock: {0}", stopwatch.ElapsedMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

Ausgabe:

With lock: 2013
Without lock: 211
With lock: 2002
Without lock: 210
With lock: 1989
Without lock: 210
With lock: 1987
Without lock: 207
With lock: 1988
Without lock: 208
Johan Nilsson
quelle
4
Dies könnte ein schlechtes Beispiel sein, da Ihre Schleife wirklich nichts tut, abgesehen von einer einzelnen Variablenzuweisung und einer Sperre von mindestens 2 Funktionsaufrufen. Außerdem sind 20 ns pro Schloss, das Sie erhalten, nicht so schlecht.
Zar Shardan
5

Es gibt verschiedene Möglichkeiten, "Kosten" zu definieren. Es gibt den tatsächlichen Aufwand für das Erhalten und Freigeben des Schlosses; Wie Jake schreibt, ist das vernachlässigbar, es sei denn, diese Operation wird millionenfach ausgeführt.

Von größerer Bedeutung ist die Auswirkung, die dies auf den Ausführungsfluss hat. Dieser Code kann jeweils nur von einem Thread eingegeben werden. Wenn Sie 5 Threads haben, die diesen Vorgang regelmäßig ausführen, warten 4 von ihnen darauf, dass die Sperre freigegeben wird, und sind dann der erste Thread, der nach der Freigabe dieser Sperre diesen Code eingibt. Ihr Algorithmus wird also erheblich leiden. Wie viel davon abhängt, hängt vom Algorithmus ab und davon, wie oft die Operation aufgerufen wird. Sie können es nicht wirklich vermeiden, ohne die Rennbedingungen einzuführen, aber Sie können es verbessern, indem Sie die Anzahl der Aufrufe des gesperrten Codes minimieren.

KeithS
quelle