Was ist der Unterschied zwischen Mutex und kritischem Abschnitt?

134

Bitte erklären Sie aus Linux-, Windows-Perspektiven?

Ich programmiere in C #, würden diese beiden Begriffe einen Unterschied machen. Bitte poste so viel wie möglich mit Beispielen und dergleichen ....

Vielen Dank


quelle

Antworten:

232

Unter Windows sind kritische Abschnitte leichter als Mutexe.

Mutexe können von Prozessen gemeinsam genutzt werden, führen jedoch immer zu einem Systemaufruf an den Kernel, der einen gewissen Overhead hat.

Kritische Abschnitte können nur innerhalb eines Prozesses verwendet werden, haben jedoch den Vorteil, dass sie nur im Konfliktfall in den Kernel-Modus wechseln. Unerwünschte Erfassungen, die der übliche Fall sein sollten, sind unglaublich schnell. Im Streitfall betreten sie den Kernel, um auf ein Synchronisationsprimitiv (wie ein Ereignis oder ein Semaphor) zu warten.

Ich habe eine kurze Beispiel-App geschrieben, die die Zeit zwischen den beiden vergleicht. Auf meinem System für 1.000.000 unerwünschte Erfassungen und Freigaben dauert ein Mutex über eine Sekunde. Ein kritischer Abschnitt dauert ~ 50 ms für 1.000.000 Erfassungen.

Hier ist der Testcode. Ich habe diesen ausgeführt und ähnliche Ergebnisse erhalten, wenn Mutex der erste oder zweite ist, sodass wir keine anderen Effekte sehen.

HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

printf("Mutex: %d CritSec: %d\n", totalTime, totalTimeCS);
Michael
quelle
1
Ich bin mir nicht sicher, ob dies damit zusammenhängt oder nicht (da ich Ihren Code nicht kompiliert und ausprobiert habe), aber ich habe festgestellt, dass das Aufrufen von WaitForSingleObject mit INFINITE zu einer schlechten Leistung führt. Wenn Sie einen Timeout-Wert von 1 übergeben und dann eine Schleife ausführen, während Sie die Rückgabe überprüfen, hat sich die Leistung einiger meiner Codes erheblich verbessert. Dies geschieht jedoch hauptsächlich im Zusammenhang mit dem Warten auf ein externes Prozesshandle ... Kein Mutex. YMMV. Mich würde interessieren, wie sich Mutex mit dieser Modifikation verhält. Der resultierende Zeitunterschied zu diesem Test scheint größer zu sein als erwartet.
Troy Howard
5
@TroyHoward bist du nicht im Grunde nur Spin Locking an diesem Punkt?
dss539
Die Gründe für diese Unterscheidung sind wahrscheinlich hauptsächlich historisch. Es ist nicht schwer, eine Sperre zu implementieren, die im unbestrittenen Fall so schnell wie CriticalSection ist (wenige atomare Anweisungen, keine Systemaufrufe), jedoch prozessübergreifend funktioniert (mit einem Teil des gemeinsam genutzten Speichers). Siehe zB Linux-Futexe .
Regnarg
2
@TroyHoward Versuchen Sie, Ihre CPU zu zwingen, die ganze Zeit zu 100% zu laufen, und prüfen Sie, ob INFINITE besser funktioniert. Die Energiestrategie kann auf meinem Computer (Dell XPS-8700) bis zu 40 ms dauern, bis sie wieder auf volle Geschwindigkeit kriecht, nachdem sie sich für eine Verlangsamung entschieden hat. Dies ist möglicherweise nicht der Fall, wenn Sie schlafen oder nur eine Millisekunde warten.
Stevens Miller
Ich bin mir nicht sicher, ob ich verstehe, was hier gezeigt wird. Im Allgemeinen erfordert das Eingeben eines kritischen Abschnitts das Erwerben einer Art Semaphor. Wollen Sie damit sagen, dass das Betriebssystem hinter den Kulissen eine effiziente Möglichkeit hat, dieses kritische Abschnittsverhalten zu implementieren, ohne Mutexe zu benötigen?
SN
89

Aus theoretischer Sicht ist ein kritischer Abschnitt ein Code, der nicht von mehreren Threads gleichzeitig ausgeführt werden darf, da der Code auf gemeinsam genutzte Ressourcen zugreift.

Ein Mutex ist ein Algorithmus (und manchmal der Name einer Datenstruktur), der zum Schutz kritischer Abschnitte verwendet wird.

Semaphoren und Monitore sind übliche Implementierungen eines Mutex.

In der Praxis stehen in Windows viele Mutex-Implementierungen zur Verfügung. Sie unterscheiden sich hauptsächlich als Folge ihrer Implementierung durch ihren Sperrgrad, ihren Umfang, ihre Kosten und ihre Leistung unter verschiedenen Streitniveaus. Ein Diagramm der Kosten verschiedener Mutex-Implementierungen finden Sie unter CLR Inside Out - Verwenden der Parallelität für die Skalierbarkeit .

Verfügbare Synchronisationsprimitive.

Die lock(object)Anweisung wird mithilfe von a implementiert Monitor- siehe MSDN als Referenz.

In den letzten Jahren wurde viel über die nicht blockierende Synchronisation geforscht . Ziel ist es, Algorithmen sperrfrei oder wartungsfrei zu implementieren. In solchen Algorithmen hilft ein Prozess anderen Prozessen, ihre Arbeit zu beenden, so dass der Prozess seine Arbeit endgültig beenden kann. Infolgedessen kann ein Prozess seine Arbeit auch dann beenden, wenn andere Prozesse, die versucht haben, einige Arbeiten auszuführen, hängen bleiben. Bei Verwendung von Sperren würden sie ihre Sperren nicht freigeben und verhindern, dass andere Prozesse fortgesetzt werden.

Daniel Brückner
quelle
Als ich die akzeptierte Antwort sah, dachte ich, ich erinnere mich vielleicht an das Konzept kritischer Abschnitte, bis ich die theoretische Perspektive sah, die Sie geschrieben haben. :)
Anirudh Ramanathan
2
Praktische sperrenfreie Programmierung ist wie Shangri La, nur dass sie existiert. Keir Frasers Artikel (PDF) untersucht dies ziemlich interessant (seit 2004). Und wir haben 2012 immer noch Probleme damit. Wir saugen.
Tim Post
22

Zusätzlich zu den anderen Antworten gelten die folgenden Details speziell für kritische Abschnitte in Windows:

  • In Ermangelung von Konflikten ist das Erfassen eines kritischen Abschnitts so einfach wie eine InterlockedCompareExchangeOperation
  • Die kritische Abschnittsstruktur bietet Platz für einen Mutex. Es ist zunächst nicht zugeordnet
  • Wenn zwischen Threads für einen kritischen Abschnitt Konflikte auftreten, wird der Mutex zugewiesen und verwendet. Die Leistung des kritischen Abschnitts verschlechtert sich zu der des Mutex
  • Wenn Sie mit hohen Konflikten rechnen, können Sie den kritischen Abschnitt unter Angabe einer Spinanzahl zuweisen.
  • Wenn ein kritischer Abschnitt mit einer Spinanzahl in Konflikt gerät, dreht sich der Thread, der versucht, den kritischen Abschnitt zu erfassen, für so viele Prozessorzyklen (Besetzt-Warten). Dies kann zu einer besseren Leistung als das Schlafen führen, da die Anzahl der Zyklen zum Ausführen eines Kontextwechsels zu einem anderen Thread viel höher sein kann als die Anzahl der Zyklen, die der besitzende Thread zum Freigeben des Mutex benötigt
  • Wenn die Spinanzahl abläuft, wird der Mutex zugewiesen
  • Wenn der besitzende Thread den kritischen Abschnitt freigibt, muss überprüft werden, ob der Mutex zugewiesen ist. Wenn dies der Fall ist, wird der Mutex so eingestellt, dass ein wartender Thread freigegeben wird

Unter Linux denke ich, dass sie eine "Spin-Sperre" haben, die einen ähnlichen Zweck erfüllt wie der kritische Abschnitt mit einer Spin-Anzahl.

1800 INFORMATIONEN
quelle
Leider beinhaltet ein Fensterkritischer Abschnitt das Ausführen einer CAS-Operation im Kernel-Modus , was massiv teurer ist als die eigentliche verriegelte Operation. Außerdem können Windows-kritischen Abschnitten Spin-Zählungen zugeordnet sein.
Promit
2
Das ist definitiv nicht wahr. CAS kann mit cmpxchg im Benutzermodus ausgeführt werden.
Michael
Ich dachte, die Standard-Spinanzahl wäre Null, wenn Sie InitializeCriticalSection aufgerufen haben - Sie müssen InitializeCriticalSectionAndSpinCount aufrufen, wenn Sie eine Spinanzahl anwenden möchten. Haben Sie eine Referenz dafür?
1800 INFORMATION
18

Critical Section und Mutex sind nicht betriebssystemspezifisch, ihre Konzepte von Multithreading / Multiprocessing.

Kritischer Abschnitt Ist ein Code, der zu einem bestimmten Zeitpunkt nur von selbst ausgeführt werden darf (z. B. werden 5 Threads gleichzeitig ausgeführt und eine Funktion namens "Critical_Section_function", die ein Array aktualisiert ... Sie möchten nicht alle 5 Threads Aktualisieren des Arrays auf einmal. Wenn das Programm also Critical_section_function () ausführt, muss keiner der anderen Threads seine Critical_section_function ausführen.

mutex * Mutex ist eine Möglichkeit, den Code für kritische Abschnitte zu implementieren (stellen Sie ihn sich wie ein Token vor ... der Thread muss ihn besitzen, um den Code für den kritischen Abschnitt auszuführen).

Das Unbekannte
quelle
2
Mutexe können auch prozessübergreifend gemeinsam genutzt werden.
Konfigurator
14

Ein Mutex ist ein Objekt, das ein Thread erfassen kann, wodurch verhindert wird, dass andere Threads ihn erfassen. Es ist beratend, nicht obligatorisch; Ein Thread kann die Ressource verwenden, die der Mutex darstellt, ohne sie zu erwerben.

Ein kritischer Abschnitt ist eine Codelänge, die vom Betriebssystem garantiert wird, dass sie nicht unterbrochen wird. Im Pseudocode wäre es wie:

StartCriticalSection();
    DoSomethingImportant();
    DoSomeOtherImportantThing();
EndCriticalSection();
Zifre
quelle
1
Ich denke, das Poster sprach über Grundelemente für die Synchronisation im Benutzermodus, wie ein Objekt mit dem kritischen Abschnitt win32, das nur einen gegenseitigen Ausschluss bietet. Ich weiß nichts über Linux, aber der Windows-Kernel hat kritische Bereiche, die sich wie von Ihnen beschrieben verhalten - nicht unterbrechbar.
Michael
1
Ich weiß nicht, warum du herabgestimmt wurdest. Es gibt das Konzept eines kritischen Abschnitts, den Sie richtig beschrieben haben und der sich von dem Windows-Kernelobjekt namens CriticalSection unterscheidet, bei dem es sich um eine Art Mutex handelt. Ich glaube, das OP hat nach der letzteren Definition gefragt.
Adam Rosenfield
Zumindest hat mich das sprachunabhängige Tag verwirrt. Auf jeden Fall erhalten wir dies, wenn Microsoft ihre Implementierung genauso benennt wie ihre Basisklasse. Schlechte Codierungspraxis!
Mikko Rantanen
Nun, er bat um so viele Details wie möglich und sagte speziell, Windows und Linux klingen so, als wären Konzepte gut. +1 - verstand die -1 auch nicht: /
Jason Coco
14

Das "schnelle" Windows, das der kritischen Auswahl unter Linux entspricht, wäre ein Futex , der für schnellen Mutex für den Benutzerbereich steht. Der Unterschied zwischen einem Futex und einem Mutex besteht darin, dass bei einem Futex der Kernel nur dann beteiligt wird, wenn ein Schiedsverfahren erforderlich ist. Sie sparen also den Aufwand, jedes Mal, wenn der Atomzähler geändert wird, mit dem Kernel zu sprechen. Dies kann in einigen Anwendungen erheblich Zeit beim Aushandeln von Sperren sparen .

Ein Futex kann auch von Prozessen gemeinsam genutzt werden, indem Sie die Mittel verwenden, die Sie zum Teilen eines Mutex verwenden würden.

Leider kann die Implementierung von Futexen sehr schwierig sein (PDF). (Update 2018, sie sind bei weitem nicht so beängstigend wie 2009).

Darüber hinaus ist es auf beiden Plattformen ziemlich gleich. Sie führen atomare, tokengesteuerte Aktualisierungen an einer gemeinsam genutzten Struktur auf eine Weise durch, die (hoffentlich) keinen Hunger verursacht. Was bleibt, ist einfach die Methode, um dies zu erreichen.

Tim Post
quelle
6

In Windows ist ein kritischer Abschnitt lokal für Ihren Prozess. Ein Mutex kann prozessübergreifend gemeinsam genutzt / aufgerufen werden. Grundsätzlich sind kritische Abschnitte viel billiger. Ich kann Linux nicht speziell kommentieren, aber auf einigen Systemen sind sie nur Aliase für dasselbe.

Promit
quelle
6

Um meine 2 Cent hinzuzufügen, werden kritische Abschnitte als Struktur definiert und Operationen im Benutzermodus ausgeführt.

ntdll! _RTL_CRITICAL_SECTION
   + 0x000 DebugInfo: Ptr32 _RTL_CRITICAL_SECTION_DEBUG
   + 0x004 LockCount: Int4B
   + 0x008 RecursionCount: Int4B
   + 0x00c OwningThread: Ptr32 Void
   + 0x010 LockSemaphore: Ptr32 Void
   + 0x014 SpinCount: Uint4B

Während Mutex Kernelobjekte (ExMutantObjectType) sind, die im Windows-Objektverzeichnis erstellt wurden. Mutex-Operationen werden meist im Kernel-Modus implementiert. Wenn Sie beispielsweise einen Mutex erstellen, rufen Sie im Kernel nt! NtCreateMutant auf.

Martin
quelle
Was passiert, wenn ein Programm, das ein Mutex-Objekt initialisiert und verwendet, abstürzt? Wird das Mutex-Objekt automatisch freigegeben? Nein, würde ich sagen. Richtig?
Ankur
6
Kernel-Objekte haben eine Referenzanzahl. Das Schließen eines Handles für ein Objekt verringert den Referenzzähler und wenn es 0 erreicht, wird das Objekt freigegeben. Wenn ein Prozess abstürzt, werden alle seine Handles automatisch geschlossen, sodass ein Mutex, für den nur dieser Prozess ein Handle hat, automatisch freigegeben wird.
Michael
Dies ist der Grund, warum Critical Section-Objekte prozessgebunden sind. Andererseits können Mutexe prozessübergreifend gemeinsam genutzt werden.
Sisir
2

Tolle Antwort von Michael. Ich habe einen dritten Test für die in C ++ 11 eingeführte Mutex-Klasse hinzugefügt. Das Ergebnis ist etwas interessant und unterstützt immer noch seine ursprüngliche Bestätigung von CRITICAL_SECTION-Objekten für einzelne Prozesse.

mutex m;
HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
m.lock();
m.unlock();

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    m.lock();
    m.unlock();
}

QueryPerformanceCounter(&end);

int totalTimeM = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);


printf("C++ Mutex: %d Mutex: %d CritSec: %d\n", totalTimeM, totalTime, totalTimeCS);

Meine Ergebnisse waren 217, 473 und 19 (beachten Sie, dass mein Zeitverhältnis für die letzten beiden ungefähr mit dem von Michael vergleichbar ist, aber meine Maschine ist mindestens vier Jahre jünger als seine, sodass Sie Hinweise auf eine höhere Geschwindigkeit zwischen 2009 und 2013 sehen können , als der XPS-8700 herauskam). Die neue Mutex-Klasse ist doppelt so schnell wie der Windows-Mutex, aber immer noch weniger als ein Zehntel der Geschwindigkeit des Windows CRITICAL_SECTION-Objekts. Beachten Sie, dass ich nur den nicht rekursiven Mutex getestet habe. CRITICAL_SECTION-Objekte sind rekursiv (ein Thread kann sie wiederholt eingeben, vorausgesetzt, er verlässt sie gleich oft).

Stevens Miller
quelle
0

AC-Funktionen werden als Wiedereintritt bezeichnet, wenn nur die tatsächlichen Parameter verwendet werden.

Wiedereintrittsfunktionen können von mehreren Threads gleichzeitig aufgerufen werden.

Beispiel für eine Wiedereintrittsfunktion:

int reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   return c;
}

Beispiel für eine nicht wiedereintretende Funktion:

int result;

void non_reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   result = c;

}

Die C-Standardbibliothek strtok () ist nicht wiedereintrittsfähig und kann nicht von zwei oder mehr Threads gleichzeitig verwendet werden.

Einige Plattform-SDKs enthalten die wiedereintrittsfähige Version von strtok () mit dem Namen strtok_r ().

Enrico Migliore

Enrico Migliore
quelle