¿Cuándo es ReaderWriterLockSlim mejor que un simple candado?

80

Estoy haciendo un punto de referencia muy tonto en ReaderWriterLock con este código, donde la lectura ocurre 4 veces más a menudo que la escritura:

class Program
{
    static void Main()
    {
        ISynchro[] test = { new Locked(), new RWLocked() };

        Stopwatch sw = new Stopwatch();

        foreach ( var isynchro in test )
        {
            sw.Reset();
            sw.Start();
            Thread w1 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w1.Start( isynchro );

            Thread w2 = new Thread( new ParameterizedThreadStart( WriteThread ) );
            w2.Start( isynchro );

            Thread r1 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r1.Start( isynchro );

            Thread r2 = new Thread( new ParameterizedThreadStart( ReadThread ) );
            r2.Start( isynchro );

            w1.Join();
            w2.Join();
            r1.Join();
            r2.Join();
            sw.Stop();

            Console.WriteLine( isynchro.ToString() + ": " + sw.ElapsedMilliseconds.ToString() + "ms." );
        }

        Console.WriteLine( "End" );
        Console.ReadKey( true );
    }

    static void ReadThread(Object o)
    {
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 500; i++ )
        {
            Int32? value = synchro.Get( i );
            Thread.Sleep( 50 );
        }
    }

    static void WriteThread( Object o )
    {
        ISynchro synchro = (ISynchro)o;

        for ( int i = 0; i < 125; i++ )
        {
            synchro.Add( i );
            Thread.Sleep( 200 );
        }
    }

}

interface ISynchro
{
    void Add( Int32 value );
    Int32? Get( Int32 index );
}

class Locked:List<Int32>, ISynchro
{
    readonly Object locker = new object();

    #region ISynchro Members

    public new void Add( int value )
    {
        lock ( locker ) 
            base.Add( value );
    }

    public int? Get( int index )
    {
        lock ( locker )
        {
            if ( this.Count <= index )
                return null;
            return this[ index ];
        }
    }

    #endregion
    public override string ToString()
    {
        return "Locked";
    }
}

class RWLocked : List<Int32>, ISynchro
{
    ReaderWriterLockSlim locker = new ReaderWriterLockSlim();

    #region ISynchro Members

    public new void Add( int value )
    {
        try
        {
            locker.EnterWriteLock();
            base.Add( value );
        }
        finally
        {
            locker.ExitWriteLock();
        }
    }

    public int? Get( int index )
    {
        try
        {
            locker.EnterReadLock();
            if ( this.Count <= index )
                return null;
            return this[ index ];
        }
        finally
        {
            locker.ExitReadLock();
        }
    }

    #endregion

    public override string ToString()
    {
        return "RW Locked";
    }
}

Pero entiendo que ambos funcionan más o menos de la misma manera:

Locked: 25003ms.
RW Locked: 25002ms.
End

Incluso haciendo la lectura 20 veces más a menudo que la escritura, el rendimiento sigue siendo (casi) el mismo.

¿Estoy haciendo algo mal aquí?

Saludos cordiales.

vtortola
fuente
3
por cierto, si saco los durmientes: "Bloqueado: 89ms. RW Bloqueado: 32ms." - o aumentando los números: "Bloqueado: 1871ms. RW Bloqueado: 2506ms.".
Marc Gravell
1
Si quito los durmientes, bloquear es más rápido que rwlocked
vtortola
1
Es porque el código que está sincronizando es demasiado rápido para crear una contención. Vea la respuesta de Hans y mi edición.
Dan Tao

Respuestas:

107

En su ejemplo, los sueños significan que generalmente no hay contención. Una cerradura no atendida es muy rápida. Para que esto importa, que se necesita un contendido de bloqueo; si hay escrituras en esa contención, deberían ser más o menos iguales ( lockpueden ser incluso más rápidas), pero si se trata principalmente de lecturas (con una contención de escritura rara vez), esperaría que el ReaderWriterLockSlimbloqueo supere al lock.

Personalmente, prefiero otra estrategia aquí, usar el intercambio de referencias, por lo que las lecturas siempre pueden leer sin verificar / bloquear / etc.Las escrituras hacen su cambio a una copia clonada , luego usan Interlocked.CompareExchangepara intercambiar la referencia (reaplicando su cambio si otro hilo mutó la referencia en el ínterin).

Marc Gravell
fuente
1
Copiar e intercambiar al escribir es definitivamente el camino a seguir aquí.
LBushkin
24
Copiar e intercambiar al escribir sin bloqueo evita por completo la contención de recursos o el bloqueo para los lectores; es rápido para los escritores cuando no hay disputas, pero puede quedar muy atascado en presencia de disputas. Puede ser bueno que los escritores adquieran un candado antes de realizar la copia (a los lectores no les importaría el candado) y que lo liberen después de realizar el intercambio. De lo contrario, si cada 100 subprocesos intentan una actualización simultánea, en el peor de los casos podrían tener que realizar un promedio de aproximadamente 50 operaciones de copia-actualización-intento-intercambio cada una (5,000 operaciones en total de copia-actualización-intento-intercambio para realizar 100 actualizaciones).
supercat
1
así es como creo que debería verse, por favor dígame si está mal:bool bDone=false; while(!bDone) { object origObj = theObj; object newObj = origObj.DeepCopy(); // then make changes to newObj if(Interlocked.CompareExchange(ref theObj, tempObj, newObj)==origObj) bDone=true; }
mcmillab
1
@mcmillab básicamente, sí; en algunos casos simples (generalmente cuando se envuelve un solo valor, o cuando no importa si luego escribe sobreescribe cosas que nunca vieron), incluso puede salirse con la suya perdiendo el Interlockedy simplemente usando un volatilecampo y simplemente asignándolo, pero si necesita estar seguro de que su cambio no se perdió por completo, entonces Interlockedes ideal
Marc Gravell
3
@ jnm2: Las colecciones usan mucho CompareExchange, pero no creo que copien mucho. El bloqueo CAS + no es necesariamente redundante, si uno lo usa para permitir la posibilidad de que no todos los escritores adquieran un bloqueo. Por ejemplo, un recurso por el cual la contención será generalmente rara pero ocasionalmente puede ser severa, podría tener un candado y una bandera que indique si los escritores deben adquirirlo. Si la bandera está clara, los escritores simplemente hacen un CAS y, si tiene éxito, siguen su camino. Sin embargo, un escritor que falle CAS más de dos veces, podría establecer la bandera; la bandera podría permanecer establecida ...
supercat
23

Mis propias pruebas indican que ReaderWriterLockSlimtiene aproximadamente 5 veces la sobrecarga en comparación con una normal lock. Eso significa que para que el RWLS supere a un candado antiguo, generalmente se producirían las siguientes condiciones.

  • El número de lectores supera significativamente al de escritores.
  • La cerradura tendría que mantenerse el tiempo suficiente para superar la sobrecarga adicional.

En la mayoría de las aplicaciones reales, estas dos condiciones no son suficientes para superar esa sobrecarga adicional. En su código específicamente, los bloqueos se mantienen durante un período de tiempo tan corto que la sobrecarga del bloqueo probablemente será el factor dominante. Si moviera esas Thread.Sleepllamadas dentro de la cerradura, probablemente obtendría un resultado diferente.

Brian Gideon
fuente
17

No hay contención en este programa. Los métodos Get y Add se ejecutan en unos pocos nanosegundos. Las probabilidades de que varios hilos lleguen a esos métodos en el momento exacto son extremadamente pequeñas.

Ponga una llamada Thread.Sleep (1) en ellos y elimine el sueño de los hilos para ver la diferencia.

Hans Passant
fuente
12

Edición 2 : Simplemente eliminando las Thread.Sleepllamadas de ReadThready WriteThread, vi un Lockedrendimiento superior RWLocked. Creo que Hans dio en el clavo aquí; sus métodos son demasiado rápidos y no crean contención. Cuando agregué Thread.Sleep(1)a los métodos Gety Addde Lockedy RWLocked(y usé 4 hilos de lectura contra 1 hilo de escritura), RWLockedquité los pantalones Locked.


Editar : OK, si realmente estuviera pensando cuando publiqué esta respuesta por primera vez, me habría dado cuenta de al menos por qué pusiste las Thread.Sleepllamadas allí: estabas tratando de reproducir el escenario de lecturas que ocurren con más frecuencia que escrituras. Esta no es la forma correcta de hacerlo. En cambio, introduciría una sobrecarga adicional a sus métodos Addy Getpara crear una mayor posibilidad de contención (como sugirió Hans ), crear más hilos de lectura que de escritura (para asegurar lecturas más frecuentes que escrituras) y eliminar las Thread.Sleepllamadas de ReadThready WriteThread(que en realidad reducir la contención, logrando lo contrario de lo que desea).


Me gusta lo que has hecho hasta ahora. Pero aquí hay algunos problemas que veo desde el principio:

  1. ¿Por qué las Thread.Sleepllamadas? Estos solo están inflando sus tiempos de ejecución en una cantidad constante, lo que hará que los resultados de rendimiento converjan artificialmente.
  2. Tampoco incluiría la creación de nuevos Threadobjetos en el código que mide su Stopwatch. Ese no es un objeto trivial de crear.

Si verá una diferencia significativa una vez que aborde los dos problemas anteriores, no lo sé. Pero creo que deberían abordarse antes de que continúe la discusión.

Dan Tao
fuente
+1: Buen punto en el n. ° 2: realmente puede sesgar los resultados (la misma diferencia entre cada uno de los tiempos, pero diferente proporción) Aunque si ejecuta esto el tiempo suficiente, el tiempo de creación de los Threadobjetos comenzará a ser insignificante.
Nelson Rothermel
10

Obtendrá un mejor rendimiento ReaderWriterLockSlimque un simple bloqueo si bloquea una parte del código que necesita más tiempo para ejecutarse. En este caso, los lectores pueden trabajar en paralelo. Adquirir un ReaderWriterLockSlimtoma más tiempo que ingresar un simple Monitor. Verifique mi ReaderWriterLockTinyimplementación para un bloqueo de lector-escritor que es incluso más rápido que una simple declaración de bloqueo y ofrece una funcionalidad de lector-escritor: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for-net/

i255
fuente
Me gusta esto. El artículo me deja preguntándome si Monitor es más rápido que ReaderWriterLockTiny en cada plataforma.
jnm2
Si bien ReaderWriterLockTiny es más rápido que ReaderWriterLockSlim (y también Monitor), hay un inconveniente: si hay muchos lectores que toman bastante tiempo, nunca les dio una oportunidad a los escritores (esperarán casi indefinidamente) .ReaderWriterLockSlim por otro lado, logran Equilibrar el acceso a recursos compartidos para lectores / escritores.
tigrou
3

Los bloqueos no controvertidos toman el orden de microsegundos para adquirir, por lo que su tiempo de ejecución se verá empequeñecido por sus llamadas a Sleep.

MSN
fuente
3

A menos que tenga hardware multinúcleo (o al menos el mismo que su entorno de producción planificado), no obtendrá una prueba realista aquí.

Una prueba más sensata sería extender la vida útil de sus operaciones bloqueadas colocando un breve retraso dentro de la cerradura. De esa manera, realmente debería poder contrastar el paralelismo agregado usando ReaderWriterLockSlimversus la serialización implícita en basic lock().

Actualmente, el tiempo que toman sus operaciones que están bloqueadas se pierde en el ruido generado por las llamadas de suspensión que ocurren fuera de las cerraduras. El tiempo total en ambos casos está relacionado principalmente con el sueño.

¿Estás seguro de que tu aplicación del mundo real tendrá el mismo número de lecturas y escrituras? ReaderWriterLockSlimes realmente mejor para el caso en el que tiene muchos lectores y escritores relativamente poco frecuentes. 1 hilo de escritor frente a 3 hilos de lector deberían demostrar ReaderWriterLockSlimmejor los beneficios, pero en cualquier caso su prueba debería coincidir con su patrón de acceso esperado en el mundo real.

Steve Townsend
fuente
2

Supongo que esto se debe al sueño que tienes en tus hilos de lector y escritor.
Su hilo de lectura tiene un sueño de 500tims 50ms, que es 25000 La mayor parte del tiempo está durmiendo

Itay Karo
fuente
0

¿Cuándo es ReaderWriterLockSlim mejor que un simple candado?

Cuando tiene significativamente más lecturas que escrituras.

Illidan
fuente