Qual è la differenza tra mutex e la sezione critica?


134

Per favore, spiega da Linux, le prospettive di Windows?

Sto programmando in C #, questi due termini farebbero la differenza. Pubblica il più possibile, con esempi e simili ....

Grazie

Risposte:


232

Per Windows, le sezioni critiche sono più leggere dei mutex.

I mutex possono essere condivisi tra i processi, ma comportano sempre una chiamata di sistema al kernel con un overhead.

Le sezioni critiche possono essere utilizzate solo all'interno di un processo, ma hanno il vantaggio di passare alla modalità kernel solo in caso di contesa: le acquisizioni indesiderate, che dovrebbero essere il caso comune, sono incredibilmente veloci. In caso di contesa, entrano nel kernel per attendere alcune primitive di sincronizzazione (come un evento o un semaforo).

Ho scritto un'app di esempio rapida che confronta il tempo tra i due. Sul mio sistema per 1.000.000 di acquisizioni e rilasci non programmati, un mutex richiede oltre un secondo. Una sezione critica richiede ~ 50 ms per 1.000.000 di acquisizioni.

Ecco il codice del test, l'ho eseguito e ho ottenuto risultati simili se mutex è il primo o il secondo, quindi non vediamo altri effetti.

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);

1
Non sono sicuro che ciò si riferisca o meno (dal momento che non ho compilato e provato il tuo codice), ma ho scoperto che chiamare WaitForSingleObject con INFINITE comporta prestazioni scadenti. Passandogli un valore di timeout di 1, quindi eseguendo il ciclo mentre si controlla il suo ritorno ha fatto una differenza enorme nelle prestazioni di alcuni dei miei codici. Questo è principalmente nel contesto dell'attesa di un handle di processo esterno, tuttavia ... Non un mutex. YMMV. Sarei interessato a vedere come si comporta il mutex con quella modifica. La differenza di tempo risultante da questo test sembra maggiore di quanto ci si aspetterebbe.
Troy Howard,

5
@TroyHoward, in pratica, non stai semplicemente ruotando a quel punto?
dss539,

Le ragioni di questa distinzione sono in gran parte storiche. Non è difficile implementare un blocco più veloce di CriticalSection nel caso non previsto (poche istruzioni atomiche, senza syscalls), ma funziona attraverso i processi (con un pezzo di memoria condivisa). Vedi ad esempio i futex di Linux .
regnarg,

2
@TroyHoward prova a forzare il funzionamento continuo della CPU al 100% e verifica se INFINITE funziona meglio. La strategia di risparmio energetico può richiedere fino a 40 ms sul mio computer (Dell XPS-8700) per tornare indietro alla massima velocità dopo aver deciso di rallentare, cosa che potrebbe non fare se dormi o aspetti solo un millisecondo.
Stevens Miller,

Non sono sicuro di capire cosa viene mostrato qui. Generalmente, entrare in una sezione critica richiede l'acquisizione di una sorta di semaforo. Stai dicendo che dietro le quinte, l'O / S ha un modo efficace di implementare questo comportamento di sezione critica senza richiedere mutex?
SN

89

Dal punto di vista teorico, una sezione critica è un pezzo di codice che non deve essere eseguito da più thread contemporaneamente perché il codice accede alle risorse condivise.

Un mutex è un algoritmo (e talvolta il nome di una struttura di dati) che viene utilizzato per proteggere sezioni critiche.

Semafori e monitor sono implementazioni comuni di un mutex.

In pratica ci sono molte implementazioni di mutex disponibili in Windows. Differiscono principalmente come conseguenza della loro implementazione per il loro livello di blocco, i loro scopi, i loro costi e le loro prestazioni in diversi livelli di contesa. Vedi CLR Inside Out - Utilizzo della concorrenza per la scalabilità per un grafico dei costi delle diverse implementazioni di mutex.

Primitive di sincronizzazione disponibili.

L' lock(object)istruzione viene implementata usando a Monitor- vedi MSDN per riferimento.

Negli ultimi anni sono state fatte molte ricerche sulla sincronizzazione non bloccante . L'obiettivo è quello di implementare algoritmi in modo lock-free o wait-free. In tali algoritmi un processo aiuta altri processi a finire il loro lavoro in modo che il processo possa finalmente finire il suo lavoro. Di conseguenza un processo può terminare il suo lavoro anche quando altri processi, che hanno tentato di eseguire qualche lavoro, si bloccano. Usando i blocchi, non rilasciano i blocchi e impediscono che altri processi continuino.


Vedendo la risposta accettata, stavo pensando che forse ho ricordato il concetto di sezioni critiche sbagliato, fino a quando ho visto quella prospettiva teorica che hai scritto. :)
Anirudh Ramanathan,

2
La pratica programmazione senza blocco è come Shangri La, tranne che esiste. L' articolo di Keir Fraser (PDF) lo esplora in modo piuttosto interessante (risalente al 2004). E stiamo ancora lottando con questo nel 2012. Facciamo schifo.
Tim Post

22

Oltre alle altre risposte, i seguenti dettagli sono specifici delle sezioni critiche su Windows:

  • in assenza di contese, acquisire una sezione critica è semplice come InterlockedCompareExchangeun'operazione
  • la struttura della sezione critica contiene spazio per un mutex. Inizialmente non è allocato
  • in caso di contesa tra thread per una sezione critica, il mutex verrà allocato e utilizzato. Le prestazioni della sezione critica diminuiranno a quelle del mutex
  • se si prevede una forte contesa, è possibile allocare la sezione critica specificando un conteggio degli spin.
  • se c'è una contesa su una sezione critica con un conteggio di spin, il thread che tenta di acquisire la sezione critica girerà (occupato) per molti cicli del processore. Ciò può comportare prestazioni migliori rispetto alla modalità di sospensione, poiché il numero di cicli per eseguire un cambio di contesto su un altro thread può essere molto più elevato del numero di cicli eseguiti dal thread proprietario per rilasciare il mutex
  • se il numero di giri scade, verrà assegnato il mutex
  • quando il thread proprietario rilascia la sezione critica, è necessario verificare se il mutex è allocato, in caso affermativo imposterà il mutex per rilasciare un thread in attesa

In Linux, penso che abbiano uno "spin lock" che ha uno scopo simile alla sezione critica con un conteggio degli spin.


Sfortunatamente una sezione critica di Windows comporta un'operazione CAS in modalità kernel , che è enormemente più costosa dell'effettiva operazione interbloccata. Inoltre, alle sezioni critiche di Windows possono essere associati conteggi di spin.
Promessa

2
Questo non è assolutamente vero. CAS può essere eseguito con cmpxchg in modalità utente.
Michael,

Pensavo che il conteggio di spin predefinito fosse zero se si chiamava InitializeCriticalSection - se si desidera applicare un conteggio di spin, è necessario chiamare InitializeCriticalSectionAndSpinCount. Hai un riferimento per questo?
1800 INFORMAZIONI

18

Critical Section e Mutex non sono specifici del sistema operativo, i loro concetti di multithreading / multiprocessing.

Sezione critica È un pezzo di codice che deve essere eseguito da solo solo in un determinato momento (ad esempio, ci sono 5 thread in esecuzione contemporaneamente e una funzione chiamata "critical_section_function" che aggiorna un array ... non vuoi tutti i 5 thread aggiornando l'array in una sola volta, quindi quando il programma esegue critical_section_function (), nessuno degli altri thread deve eseguire la funzione critical_section_function.

mutex * Mutex è un modo per implementare il codice della sezione critica (pensalo come un token ... il thread deve essere in possesso di esso per eseguire il codice critico_section_)


2
Inoltre, i mutex possono essere condivisi tra i processi.
configuratore

14

Un mutex è un oggetto che un thread può acquisire, impedendo ad altri thread di acquisirlo. È consultivo, non obbligatorio; un thread può usare la risorsa rappresentata dal mutex senza acquisirla.

Una sezione critica è una lunghezza di codice che è garantita dal sistema operativo per non essere interrotta. In pseudo-codice, sarebbe come:

StartCriticalSection();
    DoSomethingImportant();
    DoSomeOtherImportantThing();
EndCriticalSection();

1
Penso che il poster parlasse di primitive di sincronizzazione in modalità utente, come un oggetto sezione Critica win32, che fornisce solo l'esclusione reciproca. Non conosco Linux, ma il kernel di Windows ha aree critiche che si comportano come tu descrivi - non interrompibili.
Michael,

1
Non so perché sei stato sottratto. C'è il concetto di una sezione critica, che hai descritto correttamente, che è diverso dall'oggetto kernel di Windows chiamato CriticalSection, che è un tipo di mutex. Credo che il PO chiedesse di quest'ultima definizione.
Adam Rosenfield,

Almeno sono stato confuso dal tag agnostico della lingua. Ma in ogni caso questo è ciò che otteniamo per Microsoft nominando la loro implementazione come la loro classe base. Cattiva pratica di codifica!
Mikko Rantanen,

Bene, ha chiesto il maggior numero possibile di dettagli, e ha detto specificamente Windows e Linux, quindi sembra che i concetti siano buoni. +1 - non ho capito neanche il -1: /
Jason Coco il

14

L'equivalente "veloce" di Windows nella selezione critica in Linux sarebbe un futex , che significa mutex veloce per lo spazio utente. La differenza tra un futex e un mutex è che con un futex, il kernel viene coinvolto solo quando è richiesto l'arbitrato, quindi si risparmia il sovraccarico di parlare con il kernel ogni volta che si modifica il contatore atomico. Questo ... può far risparmiare un notevole periodo di tempo negoziando i blocchi in alcune applicazioni.

Un futex può anche essere condiviso tra i processi, usando i mezzi che impiegheresti per condividere un mutex.

Sfortunatamente, i futex possono essere molto difficili da implementare (PDF). (Aggiornamento del 2018, non sono così spaventosi come lo erano nel 2009).

Oltre a ciò, è praticamente lo stesso su entrambe le piattaforme. Stai apportando aggiornamenti atomici e basati su token a una struttura condivisa in modo che (si spera) non causi fame. Ciò che rimane è semplicemente il metodo per realizzarlo.


6

In Windows, una sezione critica è locale per il tuo processo. Un mutex può essere condiviso / accessibile attraverso i processi. Fondamentalmente, le sezioni critiche sono molto più economiche. Non posso commentare specificamente Linux, ma su alcuni sistemi sono solo alias per la stessa cosa.


6

Solo per aggiungere i miei 2 centesimi, le sezioni critiche sono definite come una struttura e le operazioni su di esse vengono eseguite nel contesto in modalità utente.

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

Considerando che mutex sono oggetti del kernel (ExMutantObjectType) creati nella directory degli oggetti di Windows. Le operazioni di Mutex sono per lo più implementate in modalità kernel. Ad esempio, quando si crea un Mutex, si finisce per chiamare nt! NtCreateMutant nel kernel.


Cosa succede quando un programma che inizializza e usa un oggetto Mutex, si blocca? L'oggetto Mutex viene automaticamente deallocato? No, direi. Destra?
Ankur,

6
Gli oggetti del kernel hanno un conteggio di riferimento. La chiusura di un handle per un oggetto diminuisce il conteggio dei riferimenti e quando raggiunge 0 l'oggetto viene liberato. Quando un processo si arresta in modo anomalo, tutti i suoi handle vengono chiusi automaticamente, quindi un mutex a cui solo quel processo ha un handle verrebbe automaticamente deallocato.
Michael,

E questo è il motivo per cui gli oggetti della sezione critica sono associati al processo, d'altra parte i mutex possono essere condivisi tra i processi.
Sisir,

2

Ottima risposta di Michael. Ho aggiunto un terzo test per la classe mutex introdotta in C ++ 11. Il risultato è alquanto interessante e supporta ancora la sua originale approvazione di oggetti CRITICAL_SECTION per singoli processi.

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);

I miei risultati sono stati 217, 473 e 19 (nota che il mio rapporto di volte negli ultimi due è approssimativamente paragonabile a quello di Michael, ma la mia macchina è almeno quattro anni più giovane della sua, quindi puoi vedere prove di una maggiore velocità tra il 2009 e il 2013 , quando è uscito l'XPS-8700). La nuova classe mutex è due volte più veloce del mutex di Windows, ma comunque meno di un decimo della velocità dell'oggetto CRITICAL_SECTION di Windows. Nota che ho testato solo il mutex non ricorsivo. Gli oggetti CRITICAL_SECTION sono ricorsivi (un thread può inserirli ripetutamente, purché lasci lo stesso numero di volte).


0

Le funzioni CA sono chiamate rientranti se usano solo i suoi parametri attuali.

Le funzioni rientranti possono essere richiamate da più thread contemporaneamente.

Esempio di funzione rientrante:

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

   c = a + b;

   return c;
}

Esempio di funzione non rientrante:

int result;

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

   c = a + b;

   result = c;

}

La libreria standard C strtok () non è rientrante e non può essere utilizzata da 2 o più thread contemporaneamente.

Alcune piattaforme SDK vengono fornite con la versione rientrante di strtok () chiamata strtok_r ();

Enrico Migliore

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.