¿Cuál es la diferencia entre mutex y la sección crítica?

134

¿Por favor explique desde las perspectivas de Linux y Windows?

Estoy programando en C #, ¿marcarían la diferencia estos dos términos? Por favor, publique todo lo que pueda, con ejemplos y tal ...

Gracias


fuente

Respuestas:

232

Para Windows, las secciones críticas son más livianas que las mutexes.

Los mutexes se pueden compartir entre procesos, pero siempre resultan en una llamada del sistema al kernel que tiene algo de sobrecarga.

Las secciones críticas solo se pueden usar dentro de un proceso, pero tienen la ventaja de que solo cambian al modo kernel en el caso de contención: las adquisiciones no controladas, que deberían ser el caso común, son increíblemente rápidas. En el caso de contención, ingresan al kernel para esperar alguna primitiva de sincronización (como un evento o semáforo).

Escribí una aplicación de muestra rápida que compara el tiempo entre los dos. En mi sistema por 1,000,000 de adquisiciones y lanzamientos sin supervisión, un mutex toma más de un segundo. Una sección crítica toma ~ 50 ms por 1,000,000 de adquisiciones.

Aquí está el código de prueba, ejecuté esto y obtuve resultados similares si mutex es primero o segundo, por lo que no vemos otros efectos.

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);
Miguel
fuente
1
No estoy seguro de si esto se relaciona o no (ya que no he compilado y probado su código), pero he descubierto que llamar a WaitForSingleObject con INFINITE da como resultado un bajo rendimiento. Pasarle un valor de tiempo de espera de 1 y luego hacer un ciclo mientras verifica su retorno ha marcado una gran diferencia en el rendimiento de algunos de mis códigos. Sin embargo, esto es principalmente en el contexto de esperar un identificador de proceso externo ... No es un mutex. YMMV. Me interesaría ver cómo funciona mutex con esa modificación. La diferencia de tiempo resultante de esta prueba parece mayor de lo esperado.
Troy Howard
55
@TroyHoward ¿no estás básicamente girando en ese punto?
dss539
Las razones de esta distinción son principalmente históricas. No es difícil implementar el bloqueo que es tan rápido como CriticalSection en el caso no supervisado (pocas instrucciones atómicas, sin syscalls), pero funciona en todos los procesos (con un trozo de memoria compartida). Ver, por ejemplo, futexes de Linux .
Regnarg
2
@TroyHoward intente forzar a su CPU a funcionar al 100% todo el tiempo y vea si INFINITE funciona mejor. La estrategia de energía puede demorar hasta 40 ms en mi máquina (Dell XPS-8700) para volver a la velocidad máxima después de que decida reducir la velocidad, lo que puede no hacer si duerme o espera solo un milisegundo.
Stevens Miller
No estoy seguro de entender lo que se está demostrando aquí. En general, entrar en una sección crítica requiere adquirir algún tipo de semáforo. ¿Está diciendo que detrás de escena, el O / S tiene una manera eficiente de implementar este comportamiento de sección crítica sin requerir mutexes?
SN
89

Desde una perspectiva teórica, una sección crítica es un código que no debe ser ejecutado por varios subprocesos a la vez porque el código accede a recursos compartidos.

Un mutex es un algoritmo (y, a veces, el nombre de una estructura de datos) que se utiliza para proteger secciones críticas.

Los semáforos y monitores son implementaciones comunes de un mutex.

En la práctica, hay muchas implementaciones de mutex disponibles en Windows. Difieren principalmente como consecuencia de su implementación por su nivel de bloqueo, sus alcances, sus costos y su desempeño bajo diferentes niveles de contención. Consulte CLR Inside Out: uso de la concurrencia para la escalabilidad para obtener un gráfico de los costos de las diferentes implementaciones de mutex.

Primitivas de sincronización disponibles.

La lock(object)declaración se implementa usando un Monitor- vea MSDN para referencia.

En los últimos años se ha investigado mucho sobre la sincronización sin bloqueo . El objetivo es implementar algoritmos de manera sin bloqueo o sin espera. En tales algoritmos, un proceso ayuda a otros procesos a terminar su trabajo para que el proceso finalmente pueda terminar su trabajo. En consecuencia, un proceso puede terminar su trabajo incluso cuando otros procesos, que intentaron realizar algún trabajo, se bloquean. Usando bloqueos, no liberarían sus bloqueos y evitarían que otros procesos continúen.

Daniel Brückner
fuente
Al ver la respuesta aceptada, pensé que quizás recordaba mal el concepto de secciones críticas, hasta que vi esa Perspectiva Teórica que escribiste. :)
Anirudh Ramanathan
2
La programación práctica sin bloqueo es como Shangri La, excepto que existe. El documento de Keir Fraser (PDF) explora esto de manera bastante interesante (desde 2004). Y todavía estamos luchando con eso en 2012. Somos una mierda.
Tim Post
22

Además de las otras respuestas, los siguientes detalles son específicos de las secciones críticas en Windows:

  • en ausencia de contención, adquirir una sección crítica es tan simple como una InterlockedCompareExchangeoperación
  • La estructura de sección crítica tiene espacio para un mutex. Inicialmente no está asignado
  • Si hay contención entre hilos para una sección crítica, el mutex será asignado y utilizado. El rendimiento de la sección crítica se degradará al del mutex
  • Si anticipa una alta contención, puede asignar la sección crítica que especifica un conteo de giros.
  • Si hay una disputa en una sección crítica con un conteo de giros, el hilo que intenta adquirir la sección crítica girará (espera ocupada) durante tantos ciclos de procesador. Esto puede resultar en un mejor rendimiento que dormir, ya que el número de ciclos para realizar un cambio de contexto a otro subproceso puede ser mucho mayor que el número de ciclos que toma el subproceso propietario para liberar el mutex
  • Si el conteo de giro expira, se asignará el mutex
  • cuando el subproceso propietario libera la sección crítica, es necesario verificar si el mutex está asignado, si es así, configurará el mutex para liberar un subproceso en espera

En Linux, creo que tienen un "bloqueo de giro" que sirve para un propósito similar a la sección crítica con un conteo de giro.

1800 INFORMACIÓN
fuente
Desafortunadamente, una sección crítica de Windows implica hacer una operación CAS en modo kernel , que es enormemente más costosa que la operación enclavada real. Además, las secciones críticas de Windows pueden tener recuentos de giros asociados con ellas.
Promitido
2
Eso definitivamente no es cierto. CAS se puede hacer con cmpxchg en modo de usuario.
Michael
Pensé que el conteo de giros predeterminado era cero si llamaba InitializeCriticalSection: debe llamar a InitializeCriticalSectionAndSpinCount si desea que se aplique un conteo de giros. Tienes una referencia para eso?
1800 INFORMACIÓN
18

Critical Section y Mutex no son específicos del sistema operativo, sus conceptos de multiprocesamiento / multiprocesamiento.

Sección crítica Es un fragmento de código que solo debe ejecutarse por sí mismo en un momento dado (por ejemplo, hay 5 subprocesos ejecutándose simultáneamente y una función llamada "critical_section_function" que actualiza una matriz ... no desea los 5 subprocesos actualizar la matriz de una vez. Entonces, cuando el programa está ejecutando critical_section_function (), ninguno de los otros subprocesos debe ejecutar su critical_section_function.

mutex * Mutex es una forma de implementar el código de sección crítica (piense en él como un token ... el hilo debe tener posesión para ejecutar el código_sección_crítica)

El desconocido
fuente
2
Además, los mutexes se pueden compartir entre procesos.
configurador
14

Un mutex es un objeto que un hilo puede adquirir, evitando que otros hilos lo adquieran. Es consultivo, no obligatorio; un hilo puede usar el recurso que representa el mutex sin adquirirlo.

Una sección crítica es una longitud de código que el sistema operativo garantiza que no se interrumpirá. En pseudocódigo, sería como:

StartCriticalSection();
    DoSomethingImportant();
    DoSomeOtherImportantThing();
EndCriticalSection();
Zifre
fuente
1
Creo que el póster hablaba de primitivas de sincronización en modo de usuario, como un objeto de sección crítica win32, que solo proporciona exclusión mutua. No sé acerca de Linux, pero el kernel de Windows tiene regiones críticas que se comportan como usted describe, sin interrupciones.
Michael
1
No sé por qué te votaron mal. Existe el concepto de una sección crítica, que ha descrito correctamente, que es diferente del objeto del núcleo de Windows llamado CriticalSection, que es un tipo de mutex. Creo que el OP estaba preguntando sobre la última definición.
Adam Rosenfield
Al menos me confundí con la etiqueta agnóstica del idioma. Pero, en cualquier caso, esto es lo que obtenemos para Microsoft al nombrar su implementación igual que su clase base. ¡Mala práctica de codificación!
Mikko Rantanen
Bueno, pidió la mayor cantidad de detalles posible, y dijo específicamente que Windows y Linux suenan como que los conceptos son buenos. +1 - tampoco entendió el -1: /
Jason Coco
14

El "rápido" de Windows de selección crítica en Linux sería un futex , que significa mutex de espacio de usuario rápido. La diferencia entre un futex y un mutex es que con un futex, el núcleo solo se involucra cuando se requiere arbitraje, por lo que ahorra la sobrecarga de hablar con el núcleo cada vez que se modifica el contador atómico. Eso ... puede ahorrar una cantidad significativa de tiempo negociando bloqueos en algunas aplicaciones.

Un futex también se puede compartir entre procesos, utilizando los medios que emplearía para compartir un mutex.

Desafortunadamente, los futexes pueden ser muy difíciles de implementar (PDF). (Actualización de 2018, no dan tanto miedo como en 2009).

Más allá de eso, es más o menos lo mismo en ambas plataformas. Está realizando actualizaciones atómicas impulsadas por tokens a una estructura compartida de una manera que (con suerte) no causa inanición. Lo que queda es simplemente el método para lograrlo.

Tim Post
fuente
6

En Windows, una sección crítica es local para su proceso. Un mutex se puede compartir / acceder a través de procesos. Básicamente, las secciones críticas son mucho más baratas. No puedo comentar específicamente sobre Linux, pero en algunos sistemas son solo alias para lo mismo.

Promit
fuente
6

Solo para agregar mis 2 centavos, las secciones críticas se definen como una estructura y las operaciones en ellas se realizan en el contexto del modo de usuario.

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

Mientras que mutex son objetos del núcleo (ExMutantObjectType) creados en el directorio de objetos de Windows. Las operaciones Mutex se implementan principalmente en modo kernel. Por ejemplo, al crear un Mutex, terminas llamando nt! NtCreateMutant en el núcleo.

Martín
fuente
¿Qué sucede cuando un programa que se inicializa y usa un objeto Mutex se bloquea? ¿El objeto Mutex se desasigna automáticamente? No, yo diría. ¿Correcto?
Ankur
66
Los objetos del núcleo tienen un recuento de referencia. Cerrar un identificador de un objeto disminuye el recuento de referencias y cuando alcanza 0, el objeto se libera. Cuando un proceso falla, todos sus identificadores se cierran automáticamente, por lo que un mutex que solo ese proceso tiene un identificador se desasigna automáticamente.
Michael
Y esta es la razón por la cual los objetos de la Sección Crítica están vinculados al proceso, por otro lado, los mutexes se pueden compartir entre los procesos.
Sisir
2

Gran respuesta de Michael. He agregado una tercera prueba para la clase mutex introducida en C ++ 11. El resultado es algo interesante y aún respalda su respaldo original de objetos CRITICAL_SECTION para procesos únicos.

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

Mis resultados fueron 217, 473 y 19 (tenga en cuenta que mi proporción de veces para los dos últimos es aproximadamente comparable a la de Michael, pero mi máquina es al menos cuatro años menor que la suya, por lo que puede ver evidencia de una mayor velocidad entre 2009 y 2013 , cuando salió el XPS-8700). La nueva clase mutex es dos veces más rápida que la mutex de Windows, pero aún menos de una décima parte de la velocidad del objeto CRITICAL_SECTION de Windows. Tenga en cuenta que solo probé el mutex no recursivo. Los objetos CRITICAL_SECTION son recursivos (un hilo puede ingresarlos repetidamente, siempre que salga la misma cantidad de veces).

Stevens Miller
fuente
0

Las funciones de CA se denominan reentrantes si solo utiliza sus parámetros reales.

Las funciones reentrantes pueden ser llamadas por múltiples hilos al mismo tiempo.

Ejemplo de función reentrante:

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

   c = a + b;

   return c;
}

Ejemplo de función no reentrante:

int result;

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

   c = a + b;

   result = c;

}

La biblioteca estándar C strtok () no es reentrante y no puede ser utilizada por 2 o más hilos al mismo tiempo.

Algunos SDK de plataforma vienen con la versión reentrante de strtok () llamada strtok_r ();

Enrico Migliore

Enrico Migliore
fuente