¿Qué tan cara es la declaración de bloqueo?

111

He estado experimentando con múltiples subprocesos y procesamiento paralelo y necesitaba un contador para hacer un recuento básico y un análisis estadístico de la velocidad del procesamiento. Para evitar problemas con el uso concurrente de mi clase, he usado una declaración de bloqueo en una variable privada en mi clase:

private object mutex = new object();

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

Pero me preguntaba ... ¿qué tan caro es bloquear una variable? ¿Cuáles son los efectos negativos sobre el rendimiento?

Kees C. Bakker
fuente
10
Bloquear la variable no es tan caro; es la espera en una variable bloqueada lo que desea evitar.
Gabe
53
es mucho menos costoso que pasar horas rastreando otra condición de carrera ;-)
BrokenGlass
2
Bueno ... si un candado es caro, es posible que desee evitarlo cambiando la programación para que necesite menos candados. Podría implementar algún tipo de sincronización.
Kees C. Bakker
1
Tuve una mejora dramática en el rendimiento (ahora mismo, después de leer el comentario de @Gabe) simplemente sacando mucho código de mis bloques de bloqueo. En pocas palabras: de ahora en adelante, dejaré solo el acceso variable (generalmente una línea) dentro de un bloque de bloqueo, una especie de "bloqueo justo a tiempo". ¿Tiene sentido?
heltonbiker
2
@heltonbiker Por supuesto que tiene sentido. También debe ser un principio arquitectónico, se supone que debes hacer candados lo más cortos, simples y rápidos posible. Solo datos realmente necesarios que deben sincronizarse. En las cajas de servidor, también debe tener en cuenta la naturaleza híbrida de la cerradura. La contención, incluso si no es crítica para su código, se debe a la naturaleza híbrida del bloqueo, lo que hace que los núcleos giren durante cada acceso si el bloqueo lo mantiene otra persona. Está devorando efectivamente algunos recursos de la CPU de otros servicios en el servidor durante algún tiempo antes de que se suspenda su hilo.
ipavlu

Respuestas:

86

Aquí hay un artículo que explica el costo. La respuesta corta es 50ns.

Jake Pearson
fuente
39
Respuesta corta y mejor: 50ns + tiempo de espera si otro hilo está bloqueado.
Herman
4
Cuantos más hilos entren y salgan de la cerradura, más caro se vuelve. El costo se expande exponencialmente con la cantidad de subprocesos
Arsen Zahray
16
Algo de contexto: dividir dos números en un x86 de 3Ghz toma alrededor de 10ns (sin incluir el tiempo que toma buscar / decodificar la instrucción) ; y cargar una sola variable desde la memoria (no almacenada en caché) en un registro toma alrededor de 40ns. Así que 50ns es increíblemente rápido , deslumbrantemente rápido; no debe preocuparse por el costo de usar lockmás de lo que se preocuparía por el costo de usar una variable.
BlueRaja - Danny Pflughoeft
3
Además, ese artículo era antiguo cuando se hizo esta pregunta.
Otis
3
Métrica realmente excelente, "casi sin costo", sin mencionar que es incorrecta. Ustedes no toman en consideración, que es solo corto y rápido y SOLO si no hay ninguna contención, un hilo. EN TAL CASO, NO NECESITA BLOQUEO EN ABSOLUTO. Segundo problema, el bloqueo no es un bloqueo, sino un bloqueo híbrido, detecta dentro de CLR que el bloqueo no está retenido por nadie en función de las operaciones atómicas y, en tal caso, evita las llamadas al núcleo del sistema operativo, que es un anillo diferente que no es medido por estos pruebas. Lo que se mide como 25ns a 50ns es en realidad un código de instrucciones interbloqueadas a nivel de aplicación si no se realiza el bloqueo
ipavlu
50

La respuesta técnica es que esto es imposible de cuantificar, depende en gran medida del estado de los búferes de escritura diferida de la memoria de la CPU y de cuántos datos recopilados por el prefetcher deben descartarse y volverse a leer. Ambos son muy no deterministas. Utilizo 150 ciclos de CPU como una aproximación al revés que evita grandes decepciones.

La respuesta práctica es que es mucho más barato que la cantidad de tiempo que gastarás en depurar tu código cuando crees que puedes saltarte un bloqueo.

Para obtener un número exacto, tendrá que medir. Visual Studio tiene un elegante analizador de simultaneidad disponible como extensión.

Hans Passant
fuente
1
En realidad no, se puede cuantificar y medir. Simplemente no es tan fácil como escribir esos bloqueos en todo el código y luego afirmar que todo es solo 50ns, un mito medido en el acceso de un solo hilo al bloqueo.
ipavlu
8
"Creo que puedes saltarte un candado" ... Creo que ahí es donde mucha gente se encuentra cuando lee esta pregunta ...
Snoop
30

Otras lecturas:

Me gustaría presentar algunos artículos míos que están interesados ​​en primitivas de sincronización generales y están investigando el comportamiento, las propiedades y los costos de las declaraciones de bloqueo de C #, según los distintos escenarios y la cantidad de subprocesos. Está específicamente interesado en el desperdicio de CPU y los períodos de rendimiento para comprender cuánto trabajo se puede impulsar en múltiples escenarios:

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

Respuesta original:

¡Oh querido!

Parece que la respuesta correcta marcada aquí como LA RESPUESTA es intrínsecamente incorrecta. Me gustaría pedirle al autor de la respuesta, respetuosamente, que lea el artículo enlazado hasta el final. artículo

El autor del artículo a partir de 2003 el artículo estaba midiendo en la máquina de doble núcleo y sólo en el primer caso de medición, se mide bloqueo con un solo hilo y el resultado fue de aproximadamente 50 ns por acceso a la cerradura.

No dice nada sobre un bloqueo en el entorno concurrente. Entonces tenemos que seguir leyendo el artículo y en la segunda mitad, el autor estaba midiendo el escenario de bloqueo con dos y tres subprocesos, lo que se acerca a los niveles de concurrencia de los procesadores actuales.

Entonces, el autor dice que con dos subprocesos en Dual Core, las cerraduras cuestan 120ns, y con 3 subprocesos va a 180ns. Por lo tanto, parece depender claramente del número de subprocesos que acceden al bloqueo al mismo tiempo.

Entonces es simple, no es 50 ns a menos que sea un solo hilo, donde el bloqueo se vuelve inútil.

Otro tema a considerar es que se mide como tiempo promedio .

Si se midiera el tiempo de iteraciones, habría tiempos pares entre 1 ms y 20 ms, simplemente porque la mayoría fue rápida, pero pocos subprocesos esperarán el tiempo de los procesadores e incurrirán incluso en retrasos de milisegundos.

Esta es una mala noticia para cualquier tipo de aplicación que requiera un alto rendimiento y baja latencia.

Y el último tema a considerar es que podría haber operaciones más lentas dentro de la cerradura y muy a menudo ese es el caso. Cuanto más tiempo se ejecuta el bloque de código dentro de la cerradura, mayor es la contención y los retrasos aumentan por las nubes.

Tenga en cuenta que ya ha pasado más de una década desde 2003, es decir, pocas generaciones de procesadores diseñados específicamente para funcionar de forma totalmente simultánea y el bloqueo está perjudicando considerablemente su rendimiento.

ipavlu
fuente
1
Para aclarar, el artículo no dice que el rendimiento del bloqueo se degrade con el número de subprocesos en la aplicación; el rendimiento se degrada con el número de subprocesos que compiten por el bloqueo. (Eso está implícito, pero no se indica claramente, en la respuesta anterior).
Gooseberry
Supongo que te refieres a esto: "Por lo que parece depender claramente de la cantidad de subprocesos a los que se accede simultáneamente y más es peor". Sí, la redacción podría ser mejor. Me refiero a "acceso concurrente" cuando los hilos acceden simultáneamente al bloqueo, lo que crea contención.
ipavlu
20

Esto no responde a su consulta sobre el rendimiento, pero puedo decir que .NET Framework ofrece un Interlocked.Addmétodo que le permitirá agregar su amounta su donemiembro sin bloquear manualmente otro objeto.

Adam Maras
fuente
1
Sí, probablemente esta sea la mejor respuesta. Pero principalmente por razones de código más corto y limpio. No es probable que la diferencia de velocidad se note.
Henk Holterman
gracias por esta respuesta. Estoy haciendo más cosas con cerraduras. Las entradas agregadas es una de muchas. Me encanta la sugerencia, la usaré de ahora en adelante.
Kees C. Bakker
los bloqueos son mucho, mucho más fáciles de hacer bien, incluso si el código sin bloqueo es potencialmente más rápido. Interlocked.Add solo tiene los mismos problemas que + = sin sincronización.
hangar
10

lock (Monitor.Enter / Exit) es muy barato, más barato que alternativas como Waithandle o Mutex.

Pero, ¿y si fuera (un poco) lento, preferiría tener un programa rápido con resultados incorrectos?

Henk Holterman
fuente
5
Jaja ... iba por el programa rápido y los buenos resultados.
Kees C. Bakker
@ henk-holterman Hay varios problemas con sus declaraciones: Primero, como esta pregunta y sus respuestas mostraron claramente, hay poca comprensión de los impactos del bloqueo en el rendimiento general, incluso las personas que afirman el mito sobre 50ns, que solo es aplicable en entornos de un solo subproceso. En segundo lugar, su declaración está aquí y permanecerá durante años y, mientras tanto, los procesadores crecieron en núcleos, pero la velocidad de los núcleos no tanto. ** Thrid ** aplicaciones se vuelven solo más complejas con el tiempo, y luego es capa sobre capa de bloqueo en el entorno de muchos núcleos y el número está aumentando, 2,4,8,10,20,16,32
ipavlu
Mi enfoque habitual es construir la sincronización de una manera débilmente acoplada con la menor interacción posible. Eso va muy rápido para las estructuras de datos sin bloqueo. Hice para mis envoltorios de código alrededor de spinlock para simplificar el desarrollo e incluso cuando TPL tiene colecciones concurrentes especiales, he desarrollado mis propias colecciones de bloqueo de giro alrededor de lista, matriz, diccionario y cola, ya que necesitaba un poco más de control y, a veces, algo de código ejecutándose bajo spinlock. Les puedo decir, es posible y permite resolver múltiples escenarios que las colecciones de TPL no pueden hacer y con una gran ganancia de rendimiento / rendimiento.
ipavlu
7

El costo de un candado en un circuito cerrado, en comparación con una alternativa sin candado, es enorme. Puede permitirse hacer bucles muchas veces y seguir siendo más eficiente que una cerradura. Es por eso que las colas sin bloqueo son tan eficientes.

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

Salida:

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
fuente
4
Este podría ser un mal ejemplo porque su ciclo realmente no hace nada, aparte de una sola asignación de variable y un bloqueo son al menos 2 llamadas a funciones. Además, 20ns por cerradura que obtienes no es tan malo.
Zar Shardan
5

Hay algunas formas diferentes de definir el "costo". Existe la sobrecarga real de obtener y liberar el candado; como escribe Jake, eso es insignificante a menos que esta operación se realice millones de veces.

De mayor relevancia es el efecto que esto tiene sobre el flujo de ejecución. Este código solo puede introducirse un hilo a la vez. Si tiene 5 subprocesos que realizan esta operación de manera regular, 4 de ellos terminarán esperando a que se libere el bloqueo y luego serán el primer subproceso programado para ingresar ese fragmento de código después de que se libere el bloqueo. Entonces, su algoritmo sufrirá significativamente. Cuánto depende del algoritmo y de la frecuencia con la que se llama a la operación. Realmente no puede evitarlo sin introducir condiciones de carrera, pero puede mejorarlo minimizando el número de llamadas al código bloqueado.

KeithS
fuente