web-dev-qa-db-fra.com

'using' statement vs 'try finally'

J'ai un tas de propriétés sur lesquelles je vais utiliser des verrous en lecture/écriture. Je peux les implémenter avec une clause try finally ou using.

Dans le try finally, j'obtiendrais le verrou avant la try et le relâcherais dans la finally. Dans la clause using, je créerais une classe qui acquiert le verrou dans son constructeur et la libère dans sa méthode Dispose.

J'utilise des verrous en lecture/écriture dans de nombreux endroits, donc je cherchais un moyen plus concis que try finally. J'aimerais entendre quelques idées sur les raisons pour lesquelles une méthode peut ne pas être recommandée, ou pourquoi l'une peut être meilleure qu'une autre.

Méthode 1 (try finally):

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        rwlMyLock_m .AcquireReaderLock(0);
        try
        {
            return dtMyDateTime_m
        }
        finally
        {
            rwlMyLock_m .ReleaseReaderLock();
        }
    }
    set
    {
        rwlMyLock_m .AcquireWriterLock(0);
        try
        {
            dtMyDateTime_m = value;
        }
        finally
        {
            rwlMyLock_m .ReleaseWriterLock();
        }
    }
}

Méthode 2:

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        using (new ReadLock(rwlMyLock_m))
        {
            return dtMyDateTime_m;
        }
    }
    set
    {
        using (new WriteLock(rwlMyLock_m))
        {
            dtMyDateTime_m = value;
        }
    }
}

public class ReadLock : IDisposable
{
    private ReaderWriterLock rwl;
    public ReadLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireReaderLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseReaderLock();
    }
}

public class WriteLock : IDisposable
{
    private ReaderWriterLock rwl;
    public WriteLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireWriterLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseWriterLock();
    }
}
65
Jeremy

De MSDN, using statement (référence C #)

L'instruction using garantit que Dispose est appelé même si une exception se produit pendant que vous appelez des méthodes sur l'objet. Vous pouvez obtenir le même résultat en plaçant l'objet dans un bloc try, puis en appelant Dispose dans un bloc finally. En fait, c'est ainsi que l'instruction using est traduite par le compilateur. L'exemple de code précédent se développe au code suivant au moment de la compilation (notez les accolades supplémentaires pour créer la portée limitée de l'objet):

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

Donc, fondamentalement, il s’agit du même code mais avec une vérification automatique de Nice et une portée supplémentaire pour votre variable. La documentation indique également qu'elle "garantit l'utilisation correcte de l'objet IDisposable" afin que vous puissiez également bénéficier d'une meilleure prise en charge de la structure pour les cas obscurs à l'avenir.

Alors, allez avec l'option 2.

Avoir la variable à l'intérieur d'une portée qui se termine immédiatement après que ce n'est plus nécessaire est également un plus.

80
chakrit

Je préfère définitivement la deuxième méthode. Il est plus concis au moment de l’utilisation et moins sujet aux erreurs.

Dans le premier cas, une personne qui modifie le code doit faire attention à ne rien insérer entre l'appel Verrouiller Acquérir (Lire | Écrire) et l'essai. 

(L'utilisation d'un verrou en lecture/écriture sur des propriétés individuelles comme celle-ci est généralement excessive, mais il vaut mieux l'appliquer à un niveau beaucoup plus élevé. Un simple verrou suffit souvent dans la mesure où la possibilité de conflit est probablement très faible compte tenu du temps où le verrou est maintenu. l’acquisition d’un verrou en lecture/écriture est une opération plus coûteuse qu’un simple verrou).

11
Rob Walker

Considérez la possibilité que les deux solutions soient mauvaises car elles masquent les exceptions.

Une try sans catch devrait évidemment être une mauvaise idée; voir MSDN pour savoir pourquoi l'instruction using est également dangereuse.

Notez également que Microsoft recommande maintenant ReaderWriterLockSlim au lieu de ReaderWriterLock.

Enfin, notez que les exemples Microsoft utilisent deux blocs try-catch pour éviter ces problèmes, par exemple.

try
{
    try
    {
         //Reader-writer lock stuff
    }
    finally
    {
         //Release lock
    }
 }
 catch(Exception ex)
 {
    //Do something with exception
 }

Une solution simple, cohérente et propre est un bon objectif, mais si vous ne pouvez pas simplement utiliser lock(this){return mydateetc;}, vous pouvez reconsidérer l'approche. avec plus d’informations, je suis sûr que Stack Overflow peut aider ;-)

8
Steven A. Lowe

Personnellement, j’utilise l’énoncé "using" du C # aussi souvent que possible, mais je le fais en même temps pour éviter les problèmes potentiels mentionnés. Pour illustrer:

void doSomething()
{
    using (CustomResource aResource = new CustomResource())
    {
        using (CustomThingy aThingy = new CustomThingy(aResource))
        {
            doSomething(aThingy);
        }
    }
}

void doSomething(CustomThingy theThingy)
{
    try
    {
        // play with theThingy, which might result in exceptions
    }
    catch (SomeException aException)
    {
        // resolve aException somehow
    }
}

Notez que je sépare l'instruction "using" en une méthode et l'utilisation du ou des objets en une autre avec un bloc "try"/"catch". Je peux imbriquer plusieurs instructions "using" telles que celle-ci pour des objets liés (parfois trois ou quatre fois dans mon code de production).

Dans mes méthodes Dispose() pour ces classes IDisposable personnalisées, j'attrape les exceptions (mais PAS les erreurs) et les enregistre (à l'aide de Log4net). Je n'ai jamais rencontré de situation où l'une de ces exceptions pourrait éventuellement affecter mon traitement. Comme d'habitude, les erreurs potentielles sont autorisées à se propager dans la pile d'appels et mettent généralement fin au traitement avec un message approprié (erreur et trace de pile) consigné.

Si je rencontrais d'une manière ou d'une autre une situation dans laquelle une exception significative pourrait se produire pendant Dispose(), je modifierais la conception pour cette situation. Franchement, je doute que cela se produise jamais.

Pendant ce temps, les avantages de "l'utilisation" de la portée et du nettoyage en font l'une de mes fonctionnalités C # préférées. En passant, je travaille principalement en Java, C # et Python, et beaucoup d’autres sont jeté ici et là, et "utiliser" est l’une de mes fonctions de langage les plus préférées, car c’est un outil pratique au quotidien. .

5
Rob Williams

J'aime la 3ème option

private object _myDateTimeLock = new object();
private DateTime _myDateTime;

public DateTime MyDateTime{
  get{
    lock(_myDateTimeLock){return _myDateTime;}
  }
  set{
    lock(_myDateTimeLock){_myDateTime = value;}
  }
}

Parmi vos deux options, la seconde est la plus propre et la plus facile à comprendre.

4
Will

"Bouquet de propriétés" et le verrouillage au niveau des getter et setter de la propriété semble erroné. Votre verrouillage est beaucoup trop fin. Dans la plupart des utilisations d’objets, vous voulez vous assurer que vous avez acquis un verrou pour accéder à plus / à une propriété à la fois. Votre cas spécifique pourrait être différent, mais j'en doute un peu.

Quoi qu'il en soit, l'acquisition du verrou lorsque vous accédez à l'objet au lieu de la propriété réduira considérablement la quantité de code de verrouillage que vous devrez écrire.

4
Hans Passant

SEC dit: deuxième solution. La première solution duplique la logique d'utilisation d'un verrou, tandis que la seconde ne le fait pas.

3
Tetha

Les blocs Try/Catch sont généralement destinés à la gestion des exceptions, tandis que des blocs sont utilisés pour garantir la suppression de l'objet.

Pour le verrou en lecture/écriture, un try/catch peut être le plus utile, mais vous pouvez aussi utiliser les deux, comme ceci:

using (obj)
{
  try { }
  catch { }
}

afin que vous puissiez appeler implicitement votre interface IDisposable et rendre la gestion des exceptions concise.

1
Luke

Je suis surpris que personne n'ait suggéré d'encapsuler try-finally dans des fonctions anonymes. Tout comme la technique d'instanciation et d'élimination de classes avec l'instruction using, le verrouillage est maintenu au même endroit. Je préfère cela moi-même uniquement parce que je préfère lire le mot "finalement" que le mot "Éliminer" lorsque je songe à libérer un verrou.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                rwlMyLock_m,
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                rwlMyLock_m,
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private static DateTime ReadLockedMethod(
        ReaderWriterLock rwl,
        ReadLockMethod method
    )
    {
        rwl.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwl.ReleaseReaderLock();
        }
    }

    private static void WriteLockedMethod(
        ReaderWriterLock rwl,
        WriteLockMethod method
    )
    {
        rwl.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwl.ReleaseWriterLock();
        }
    }
}
0
Matthew

SoftwareJedi, je n'ai pas de compte, je ne peux donc pas modifier mes réponses.

Dans tous les cas, la version précédente ne convenait pas vraiment à une utilisation générale, car le verrou de lecture nécessitait toujours une valeur de retour. Cela corrige que:

class StackOTest
{
    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            DateTime retval = default(DateTime);
            ReadLockedMethod(
                delegate () { retval = dtMyDateTime_m; }
            );
            return retval;
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private void ReadLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}
0
Matthew

En fait, dans votre premier exemple, pour rendre les solutions comparables, vous devriez également implémenter "IDisposable" et appeler "Dispose" à partir du bloc "Finally" au lieu de relâcher directement le verrou. Dans ce cas, vous utiliserez la méthode "pomme à pomme" (et MSIL) (MSIL sera identique pour les deux solutions). C’est quand même une bonne idée d’utiliser «utiliser» en raison de la portée ajoutée et du fait que le cadre garantira l’utilisation appropriée d’Idéposable (ce dernier étant moins utile si vous implémentez vous-même «IDisposabe»).

0
user1172173

Je pense que la méthode 2 serait mieux.

  • Code plus simple et plus lisible dans vos propriétés.
  • Moins sujet aux erreurs puisque le code de verrouillage n'a pas besoin d'être réécrit plusieurs fois.
0
Nathen Silver

Ce qui suit crée des méthodes d'extension pour la classe ReaderWriterLockSlim qui vous permettent d'effectuer les opérations suivantes:

var rwlock = new ReaderWriterLockSlim();
using (var l = rwlock.ReadLock())
{
     // read data
}
using (var l = rwlock.WriteLock())
{
    // write data
}

Voici le code:

static class ReaderWriterLockExtensions() {
    /// <summary>
    /// Allows you to enter and exit a read lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitReadLock on dispose</returns>
    public static OnDispose ReadLock(this ReaderWriterLockSlim readerWriterLockSlim)
    {
        // Enter the read lock
        readerWriterLockSlim.EnterReadLock();
        // Setup the ExitReadLock to be called at the end of the using block
        return new OnDispose(() => readerWriterLockSlim.ExitReadLock());
    }
    /// <summary>
    /// Allows you to enter and exit a write lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitWriteLock on dispose</returns>
    public static OnDispose WriteLock(this ReaderWriterLockSlim rwlock)
    {
        // Enter the write lock
        rwlock.EnterWriteLock();
        // Setup the ExitWriteLock to be called at the end of the using block
        return new OnDispose(() => rwlock.ExitWriteLock());
    }
}

/// <summary>
/// Calls the finished action on dispose.  For use with a using statement.
/// </summary>
public class OnDispose : IDisposable
{
    Action _finished;

    public OnDispose(Action finished) 
    {
        _finished = finished;
    }

    public void Dispose()
    {
        _finished();
    }
}
0
Clint Chapman

Bien que je sois d’accord avec nombre des commentaires ci-dessus, y compris la granularité du verrou et la gestion des exceptions discutables, la question en est une d’approche. Laissez-moi vous expliquer en détail pourquoi je préfère utiliser le modèle try {} finally ... l'abstraction.

J'ai un modèle très similaire au vôtre à une exception près. J'ai défini une interface de base ILock dans laquelle j'ai fourni une méthode appelée Acquire (). La méthode Acquire () a renvoyé l'objet IDisposable. Par conséquent, tant que l'objet avec lequel je traite est de type ILock, il peut être utilisé pour verrouiller une étendue. Pourquoi est-ce important?

Nous traitons avec beaucoup de mécanismes de verrouillage et de comportements différents. Votre objet de verrouillage peut avoir un délai d'expiration spécifique qui utilise. Votre implémentation de verrouillage peut être un verrou de moniteur, de lecteur, de graveur ou de verrou de rotation. Toutefois, du point de vue de l'appelant, tout cela n'est pas pertinent. Ce qui leur importe, c'est que le contrat de verrouillage de la ressource soit honoré et que le verrouillage le fasse de manière cohérente avec sa mise en œuvre.

interface ILock {
    IDisposable Acquire();
}

class MonitorLock : ILock {
    IDisposable Acquire() { ... acquire the lock for real ... }
}

J'aime votre modèle, mais j'aimerais envisager de cacher le mécanisme de verrouillage à l'appelant. FWIW, j’ai mesuré les frais généraux de la technique d’utilisation par rapport à ceux de l’essai final. Les frais généraux liés à l’affectation de l’objet jetable entraînent une surcharge de performances de 2-3%.

0
Ajaxx