c++ - sincronizacion - semaforos sistemas operativos
¿La sección crítica siempre es más rápida? (6)
Aquí hay una manera de verlo:
Si no hay contención, entonces el bloqueo de giro es realmente rápido en comparación con ir al modo kernel para un Mutex.
Cuando hay una disputa, una sección crítica es un poco más costosa que usar un Mutex directamente (debido al trabajo adicional para detectar el estado de bloqueo).
Por lo tanto, se reduce a un promedio ponderado, donde los pesos dependen de los detalles de su patrón de llamadas. Dicho eso, si tienes poca opinión, entonces una CriticalSection es una gran victoria. Si, por otro lado, constantemente tienes muchas disputas, entonces estarías pagando una pequeña penalización por usar un Mutex directamente. Pero en ese caso, lo que ganarías al cambiar a un Mutex es pequeño, por lo que probablemente sería mejor que intentaras reducir la contienda.
Estaba depurando una aplicación multiproceso y encontré la estructura interna de CRITICAL_SECTION
. Encontré al miembro de datos LockSemaphore
de CRITICAL_SECTION uno interesante.
Parece que LockSemaphore
es un evento de restablecimiento automático (no un semáforo, como su nombre indica) y el sistema operativo crea este evento de forma silenciosa la primera vez que un hilo espera en la Critcal Section
que está bloqueada por algún otro hilo.
Ahora, me pregunto si la sección crítica siempre es más rápida. El evento es un objeto del kernel y cada objeto de la sección Critical se asocia con el objeto del evento, ¿cómo Critical Section
puede ser más rápida en comparación con otros objetos del núcleo como Mutex? Además, ¿cómo afecta el objeto de evento interno el rendimiento de la sección Crítica?
Aquí está la estructura de CRITICAL_SECTION
:
struct RTL_CRITICAL_SECTION
{
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread;
HANDLE LockSemaphore;
ULONG_PTR SpinCount;
};
CriticalSections es más rápido, pero InterlockedIncrement / InterlockedDecrement es más. Vea esta copia de ejemplo de uso de implementación LightweightLock .
Cuando dicen que una sección crítica es "rápida", significan que "es barato adquirir una cuando no está bloqueada por otra cadena".
[Tenga en cuenta que si ya está bloqueado por otro hilo, entonces no importa tanto qué tan rápido sea.]
La razón por la que es rápido es porque, antes de entrar en el kernel, usa el equivalente de InterlockedIncrement
en uno de esos campos LONG
(quizás en el campo LockCount
) y si tiene éxito, considera el bloqueo adquirido sin haber ingresado en el núcleo .
La API InterlockedIncrement
está implementada en el modo de usuario como un código de operación "LOCK INC" ... en otras palabras, puede adquirir una sección crítica no cuestionada sin hacer ninguna transición de anillo en el kernel.
En el trabajo de rendimiento, pocas cosas caen en la categoría "siempre" :) Si implementa algo usted mismo que es similar a una sección crítica del sistema operativo utilizando otras primitivas, entonces las probabilidades son que serán más lentas en la mayoría de los casos.
La mejor manera de responder a su pregunta es con mediciones de rendimiento. El rendimiento de los objetos de SO depende mucho del escenario. Por ejemplo, las secciones críticas son generalmente consideradas "rápidas" si la contención es baja. También se consideran rápidos si el tiempo de bloqueo es menor que el tiempo de conteo de vueltas.
Lo más importante para determinar es si la contención en una sección crítica es el factor limitante de primer orden en su aplicación. De lo contrario, simplemente use una sección crítica de manera normal y trabaje en el cuello de botella (o cuellos) principal de sus aplicaciones.
Si el rendimiento crítico de la sección es crítico, entonces puede considerar lo siguiente.
- Con cuidado, configure el recuento de bloqueo de centrifugado para sus secciones críticas "calientes". Si el rendimiento es primordial, entonces el trabajo aquí lo vale. Recuerde, mientras que el bloqueo de giro evita el modo de usuario para la transición del kernel, consume tiempo de CPU a un ritmo vertiginoso: mientras gira, nada más puede usar ese tiempo de CPU. Si se mantiene un bloqueo durante el tiempo suficiente, entonces el hilo giratorio bloqueará realmente, liberando esa CPU para hacer otro trabajo.
- Si tiene un patrón de lector / escritor, entonces considere usar los bloqueos Slim Reader / Writer (SRW) . La desventaja aquí es que solo están disponibles en Vista y Windows Server 2008 y productos posteriores.
- Es posible que pueda usar variables de condición con su sección crítica para minimizar el sondeo y la contención, y que se activen solo cuando sea necesario. Nuevamente, estos son compatibles con Vista y Windows Server 2008 y productos posteriores.
- Considere la posibilidad de utilizar Listas Enlazadas Singularmente Enlazadas (SLIST), que son eficientes y "sin bloqueo". Aún mejor, son compatibles con XP y Windows Server 2003 y productos posteriores.
- Examine su código: es posible que pueda romper un bloqueo "caliente" refactorizando algún código y utilizando una operación interconectada, o SLIST para sincronización y comunicación.
En resumen, los escenarios de ajuste que tienen contención de bloqueo pueden ser un trabajo desafiante (¡pero interesante!). Concéntrese en medir el rendimiento de sus aplicaciones y comprender dónde están sus rutas calientes. Las herramientas xperf en el Kit de herramientas de rendimiento de Windows son su amigo aquí :) Acabamos de lanzar la versión 4.5 en el SDK de Microsoft Windows para Windows 7 y .NET Framework 3.5 SP1 ( ISO está aquí , instalador web aquí ). Puede encontrar el foro para las herramientas xperf here . V4.5 es totalmente compatible con Win7, Vista, Windows Server 2008 - todas las versiones.
La sección crítica es más rápida que mutex porque la sección crítica no es un objeto kernel. Esto es parte de la memoria global del proceso actual. Mutex en realidad reside en Kernel y la creación del objeto mutext requiere un cambio de núcleo, pero en el caso de la sección crítica no. Aunque la sección crítica es rápida, habrá un cambio de kernel al usar la sección crítica cuando los hilos vayan a esperar el estado. Esto se debe a que la programación de hilos ocurre en el lado del kernel.
The CriticalSections girará brevemente (unos pocos ms) y seguirá comprobando si el bloqueo es gratis. Después de que el recuento de centrifugados agote el tiempo de espera, volverá al evento kernel. Entonces, en el caso en que el titular de la cerradura salga rápidamente, nunca tendrá que realizar la costosa transición al código kernel.
EDITAR: Fui y encontré algunos comentarios en mi código: al parecer, el MS Heap Manager utiliza un recuento de vueltas de 4000 (incrementos enteros, no ms)