web-dev-qa-db-fra.com

Quand ReaderWriterLockSlim est-il meilleur qu'un simple verrou?

Je fais une référence très stupide sur le ReaderWriterLock avec ce code, où la lecture se produit 4x plus souvent que l'écriture:

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";
    }
}

Mais je comprends que les deux fonctionnent plus ou moins de la même manière:

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

Même en faisant la lecture 20 fois plus souvent qu’elle écrit, la performance est toujours (presque) la même. 

Est-ce que je fais quelque chose de mal ici?

Sincères amitiés.

70
vtortola

Dans votre exemple, les mises en sommeil signifient que généralement, il n'y a pas de conflit. Un verrou non contrôlé est très rapide. Pour que cela soit important, il vous faudrait un verrou contended; s'il y a écrit dans ce conflit, ils devraient être à peu près identiques (lock peut même être plus rapide) - mais s'il s'agit de most lit (avec un conflit d'écriture rarement), je m'attendrais au verrou ReaderWriterLockSlim surpasser la lock.

Personnellement, je préfère une autre stratégie ici, l’utilisation de la permutation de références. Les lectures peuvent donc toujours lire sans même vérifier/verrouiller/etc. Les écritures changent en une copie cloned, puis utilisent Interlocked.CompareExchange pour permuter la référence (réapplication). leur changement si un autre thread a muté la référence dans l’intervalle).

93
Marc Gravell

Mes propres tests indiquent que ReaderWriterLockSlim a environ 5 fois les frais généraux par rapport à une lock normale. Cela signifie que, pour que le RWLS surpasse un vieux verrou ordinaire, les conditions suivantes seraient généralement remplies.

  • Le nombre de lecteurs est nettement supérieur aux écrivains.
  • La serrure devrait être maintenue assez longtemps pour surmonter les frais généraux supplémentaires.

Dans la plupart des applications réelles, ces deux conditions ne suffisent pas pour surmonter cette surcharge supplémentaire. Dans votre code en particulier, les verrous sont conservés pendant une période de temps si courte que le temps système de verrouillage sera probablement le facteur dominant. Si vous déplaciez ces appels Thread.Sleep à l'intérieur du verrou, vous obtiendrez probablement un résultat différent.

20
Brian Gideon

Il n'y a pas de conflit dans ce programme. Les méthodes Get et Add s'exécutent en quelques nanosecondes. Les chances que plusieurs threads atteignent ces méthodes à l'heure exacte sont infimes.

Mettez un Thread.Sleep (1) appelez-les et supprimez le sommeil des threads pour voir la différence.

14
Hans Passant

Edit 2: En supprimant simplement les appels Thread.Sleep de ReadThread et WriteThread, j'ai vu Locked surpasser RWLocked. Je crois que Hans a mis le doigt sur la tête ici; vos méthodes sont trop rapides et ne créent pas de conflit. Lorsque j'ai ajouté Thread.Sleep(1) aux méthodes Get et Add de Locked et RWLocked (et utilisé 4 threads en lecture contre 1 en écriture), RWLocked a dépassé le seuil de Locked.


Edit: OK, si j’avais eu en train de penser à lorsque j’ai posté cette réponse pour la première fois, j’aurais au moins réalisé pourquoi vous avez mis les appels Thread.Sleep ici: vous essayez de reproduire le scénario de lectures plus fréquentes que d’écritures. Ce n'est tout simplement pas la bonne façon de faire cela. Au lieu de cela, j'introduirais une surcharge supplémentaire dans vos méthodes Add et Get pour créer une plus grande probabilité de conflit (en tant que Hans suggéré ), créer plus de threads lus que d'écriture (pour assurer des lectures plus fréquentes que des écritures), et supprimez les appels Thread.Sleep de ReadThread et WriteThread (ce qui en fait réduit les conflits, obtenant ainsi le contraire de ce que vous voulez).


J'aime ce que vous avez fait jusqu'à présent. Mais voici quelques problèmes que je vois d’emblée:

  1. Pourquoi l'appel Thread.Sleep? Cela ne fait que gonfler vos temps d'exécution d'un montant constant, ce qui va artificiellement faire converger les résultats de performance.
  2. Je n'inclurais pas non plus la création de nouveaux objets Thread dans le code mesuré par votre Stopwatch. Ce n'est pas un objet trivial à créer.

Je ne sais pas si vous constaterez une différence significative une fois que vous aurez abordé les deux problèmes mentionnés ci-dessus. Mais je crois qu’il faut les aborder avant que la discussion ne continue.

9
Dan Tao

ReaderWriterLockSlim permet d'obtenir de meilleures performances qu'un simple verrouillage si vous verrouillez une partie du code qui a besoin de plus de temps pour s'exécuter. Dans ce cas, les lecteurs peuvent travailler en parallèle. L'acquisition d'une ReaderWriterLockSlim prend plus de temps que d'entrer une simple Monitor. Vérifiez ma mise en œuvre ReaderWriterLockTiny pour un verrou lecteurs-écrivain qui est encore plus rapide qu'une simple instruction de verrouillage et offre une fonctionnalité lecteurs-écrivain: http://i255.wordpress.com/2013/10/05/fast-readerwriterlock-for- net/

8
i255

Découvrez cet article: http://blogs.msdn.com/b/pedram/archive/2007/10/07/a-performance-comparison-of-readerwriterlockslim-with-readerwriterlock.aspx

Vos nuits sont probablement suffisamment longues pour rendre votre verrouillage/déverrouillage statistiquement insignifiant.

3
Nelson Rothermel

Uncontested les verrous prennent en ordre de microsecondes à acquérir, votre temps d'exécution sera donc réduit au minimum par vos appels à Sleep.

3
MSN

Sauf si vous disposez d'un matériel multicœur (ou du moins identique à votre environnement de production prévu), vous ne réaliserez pas de test réaliste ici.

Un test plus judicieux serait de prolonger la durée de vie de vos opérations verrouillées en mettant un bref délai à l'intérieur le verrou. De cette façon, vous devriez vraiment pouvoir contraster le parallélisme ajouté en utilisant ReaderWriterLockSlim et la sérialisation impliquée par lock() de base. 

Actuellement, le temps pris par vos opérations verrouillées est perdu dans le bruit généré par les appels de veille qui se produisent en dehors des verrous. Dans les deux cas, le temps total est principalement lié au sommeil.

Êtes-vous sûr que votre application réelle aura un nombre égal de lectures et d'écritures? ReaderWriterLockSlim est vraiment préférable dans le cas où vous avez beaucoup de lecteurs et d'écrivains relativement peu fréquents. Un thread d'écriture contre trois threads de lecteur devraient mieux démontrer les avantages de ReaderWriterLockSlim, mais dans tous les cas, votre test doit correspondre au modèle d'accès réel attendu.

3
Steve Townsend

Je suppose que cela est dû au sommeil que vous avez en vous, lecteurs et écrivains.
Votre fil de lecture a une durée de sommeil de 500 ms 50ms, soit 25 000 La plupart du temps, il dort

2
Itay Karo

Quand ReaderWriterLockSlim est-il meilleur qu'un simple verrou?

Lorsque vous avez beaucoup plus de lectures que d'écritures. 

0
Illidan