63 votes

Gel d'une glace à l'eau .NET (faire une classe immuable)

Je suis de la conception d'une classe que je souhaite faire en lecture seule après un thread principal est fait de la configuration, c'est à dire de "geler". Eric Lippert appelle cette popsicle immuabilité. Après il est congelé, il peut être accédé par plusieurs threads en même temps pour la lecture.

Ma question est de savoir comment écrire un thread-safe manière réaliste efficace, c'est à dire sans essayer d'être inutilement intelligent.

Tentative 1:

public class Foobar
{
   private Boolean _isFrozen;

   public void Freeze() { _isFrozen = true; }

   // Only intended to be called by main thread, so checks if class is frozen. If it is the operation is invalid.
   public void WriteValue(Object val)
   {
      if (_isFrozen)
         throw new InvalidOperationException();

      // write ...
   }

   public Object ReadSomething()
   {
      return it;
   }
}

Eric Lippert semble suggérer ce serait OK dans cepost. Je sais que les écritures sont la libération de la sémantique, mais autant je comprends cela s'applique uniquement à la commande, et il ne veut pas nécessairement dire que tous les threads vont voir la valeur immédiatement après l'écriture. Quelqu'un peut-il confirmer cela? Cela signifierait que cette solution n'est pas thread-safe (cela peut ne pas être la seule raison, bien sûr).

Tentative 2:

Ci-dessus, mais en utilisant Interlocked.Exchange afin de s'assurer que la valeur est en fait publié:

public class Foobar
{
   private Int32 _isFrozen;

   public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }

   public void WriteValue(Object val)
   {
      if (_isFrozen == 1)
         throw new InvalidOperationException();

      // write ...
   }
}

L'avantage ici serait de nous assurer que la valeur est publiée à l'insu de la souffrance le dessus sur tous les lire. Si aucune de ces lectures sont déplacés avant l'écriture de _isFrozen comme le Contrefil méthode utilise une mémoire pleine de barrière, je suppose que c'est thread-safe. Cependant, qui sait ce que le compilateur va faire (et conformément à la section 3.10 du C# spec qui semble comme beaucoup), donc je ne sais pas si c'est thread-safe.

Tentative 3:

Également faire de la lecture à l'aide de Interlocked.

public class Foobar
{
   private Int32 _isFrozen;

   public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }

   public void WriteValue(Object val)
   {
      if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
         throw new InvalidOperationException();

      // write ...
   }
}

Certainement pas thread-safe, mais il semble un peu fastidieux de faire l'échange de comparaison pour tous les lire. Je sais que cette surcharge est probablement minime, mais je suis à la recherche d'un raisonnablement efficace méthode (bien que peut-être ce qu'il est).

Tentative 4:

À l'aide de volatile:

public class Foobar
{
   private volatile Boolean _isFrozen;

   public void Freeze() { _isFrozen = true; }

   public void WriteValue(Object val)
   {
      if (_isFrozen)
         throw new InvalidOperationException();

      // write ...
   }
}

Mais Joe Duffy a déclaré "sayonara volatile", donc je ne vais pas envisager cette solution.

Tentative de 5:

Verrouiller tout, semble un peu exagéré:

public class Foobar
{
   private readonly Object _syncRoot = new Object();
   private Boolean _isFrozen;

   public void Freeze() { lock(_syncRoot) _isFrozen = true; }

   public void WriteValue(Object val)
   {
      lock(_syncRoot) // as above we could include an attempt that reads *without* this lock
         if (_isFrozen)
            throw new InvalidOperationException();

      // write ...
   }
}

Aussi semble définitivement thread-safe, mais a plus de généraux que d'utiliser le Contrefil approche ci-dessus, donc je serais favorable à tenter 3 sur celui.

Et puis je peux venir avec au moins un peu plus (je suis sûr qu'il ya beaucoup d'autres):

Tentative de 6: utilisation Thread.VolatileWrite et Thread.VolatileRead, mais ce sont des soi-disant un peu sur le côté le plus lourd.

Tentative de 7: utilisation Thread.MemoryBarrier, me semble un peu trop interne.

Tentative 8: créer un immuable la copie ne veux pas faire ce

Résumant:

  • qui tentent utilisez-vous et pourquoi (ou comment auriez-vous fait si totalement différent)? (c'est à dire quel est le meilleur moyen pour la publication d'une valeur une fois qui est ensuite lu simultanément, tout en étant raisonnablement efficace sans être trop "sage"?)
  • n' .NET de la mémoire du modèle de la "libération" de la sémantique de l'écrit implique que tous les autres threads voir les mises à jour (de cohérence de cache, etc.)? En général, je ne veux pas trop penser à cela, mais c'est agréable d'avoir une compréhension de.

EDIT:

Peut-être que ma question n'était pas claire, mais je suis à la recherche, en particulier pour les raisons ci-dessus tentatives sont bonnes ou mauvaises. Notez que je parle ici de l'un scénario d'un seul écrivain qui écrit puis gèle avant, toutes les lectures. Je crois tentative 1 est OK mais je voudrais savoir exactement pourquoi (je me demande si les lectures peuvent être optimisés à l'écart d'une certaine manière, par exemple). J'ai moins de soins sur si oui ou non c'est une bonne pratique de conception, mais plus sur la réelle de filetage aspect de celui-ci.


Merci beaucoup pour la réponse à la question, mais j'ai choisi de célébrer cela comme une réponse moi-même, parce que je pense que les réponses données ne sont pas tout à fait répondu à ma question et je ne veux pas donner l'impression à tous ceux qui visitent le site que la réponse est correcte simplement parce qu'il a été automatiquement marqués comme tels en raison de l'abondance d'expirer. En outre, je ne pense pas que la réponse avec le plus grand nombre de voix a été massivement voté pour, pas assez pour marquer automatiquement comme une réponse.

Je suis toujours appuyé à la tentative #1 est correcte, cependant, j'aurais aimé avoir quelques réponses faisant autorité. Je comprends x86 a un modèle fort, mais je ne veux pas (et ne devrait pas) code pour une architecture particulière, après tout c'est une des belles choses au sujet de .NET.

Si vous êtes dans le doute au sujet de la réponse, optez pour l'une de verrouillage approches, peut-être avec les optimisations présentées ici à éviter beaucoup de conflits sur la serrure.

25voto

Al3x3 Points 233

Peut-être un peu du sujet, mais juste par curiosité :) Pourquoi n'utilisez-vous pas de "vrais" immutabilité? par exemple, faire de Gel() retourne un immuable copie (sans les "méthodes d'écriture", ou toute autre possibilité de changement de l'état interne) et l'utilisation de cette copie à la place de l'objet d'origine. On pourrait même aller sans modification de l'état et de retourner une nouvelle copie (avec le changement de l'état) sur chaque opération d'écriture de la place (autant que je sache, la classe string fonctionne comme ça). "La vraie immutabilité" est intrinsèquement thread-safe.

8voto

csharptest.net Points 16556

Je vote pour Tenter de 5, utilisation de la serrure(ce) de la mise en œuvre.

C'est le plus efficace et fiable, c'est de faire de ce travail. Lecteur/enregistreur de serrures pourraient être utilisées, mais pour très peu de gain. Juste aller avec, le verrouiller.

Si nécessaire, vous pourriez améliorer la "gelée" la performance par la première vérification de l' _isFrozen , puis le verrouillage ou de passage pour lire uniquement code. En essence, la classe devrait ressembler à la suivante:

void Freeze() { lock (this) _isFrozen = true; }
object ReadValue()
{
    if (_isFrozen)
        return Read();
    else
        lock (this) return Read();
}
void WriteValue(object value)
{
    lock (this)
    {
        if (_isFrozen) throw new InvalidOperationException();
        Write(value);
    }
}

-- update --

Si vous êtes en train de jouer et que vous voulez vraiment essayer cette lockless/sans verrouillage, vous devez toujours fournir les moyens de les attendre sur les écrivains 'gel' de la liste. (Pour info ceci est également connu comme un "état variable", provoquant le Gel() pour attendre une condition. Ils sont difficiles à mettre en œuvre correctement, mais ce qui suit est une démonstration d'une "minime" version)

public class Foobar
{
  private Int32 _isFrozen;
  private Int32 _writers;

  public void Freeze()
  { 
     Interlocked.Exchange(ref _isFrozen, 1); 
     // Wait for the writers to exit
     while (Interlocked.CompareExchange(ref _writers, 0, 0) != 0)
        Thread.Yield();
  }

  public void WriteValue(Object val)
  {
     if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
        throw new InvalidOperationException();

     Interlocked.Increment(ref _writers);
     try
     {
        if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
           throw new InvalidOperationException();

        // write ...
     }
     finally
     {
        Interlocked.Decrement(ref _writers);
     }
  }
}

Bien que cela fonctionne sous "normal" de circonstances, la question de l'exécution dans la qu'elle il est possible pour le Gel() appel à ne jamais quitter. Là encore, c'est seulement pour la démonstration, je vous recommande d'utilisation de la serrure pour toute la production de code.

5voto

Paulo Zemek Points 31

Si vous avez vraiment de créer, de le remplir et de gel de l'objet avant de le montrer à d'autres threads, alors vous n'avez pas besoin de rien de spécial à faire face avec du fil de sécurité (la bonne mémoire de modèle .NET est déjà votre garantie), de sorte que la solution 1 est valide.

Mais, si vous donnez la dégelée objet à un autre thread (ou si vous êtes à la simple création de votre classe, sans savoir comment les utilisateurs vont l'utiliser), puis à l'aide de la version la solution qui retourne un nouveau pleinement immuable exemple est probablement mieux. Dans ce cas, la Mutable exemple, c'est comme le StringBuilder et l'immuable exemple, c'est comme la chaîne. Si vous avez besoin d'une garantie supplémentaire, la mutable instance peut vérifier son créateur fil et de lancer des exceptions si il est utilisé à partir de n'importe quel autre thread (dans toutes les méthodes... pour éviter d'éventuels partielle lit).

2voto

Yaur Points 4362

Tentative 2 est thread-safe sur x86 et d'autres processeurs qui ont une bonne mémoire de modèle, mais comment je pourrais faire c'est de faire de la sécurité des threads consommateurs de problème car il n'existe aucun moyen pour vous de manière efficace de le faire dans le consommé de code. Considérer:

if(!foo.frozen)
{
    foo.apropery = "avalue";
}

le thread de sécurité de l' frozen de la propriété et la garde de code en aproperys'setter n'a pas vraiment d'importance parce que, même s'ils sont parfaitement thread-safe, vous avez encore une condition de course. Au lieu de cela, je voudrais écrire comme

lock(foo)
{
    if(!foo.frozen)
    {
        foo.apropery = "avalue";
    }
}

et n'ont ni les propriétés intrinsèquement thread-safe.

1voto

ChrisW Points 37322

Est la chose construite et écrite, puis gelé en permanence et de lire plusieurs fois?

Ou avez-vous geler et dégeler et recongeler plusieurs fois?

Si c'est l'ancien, alors peut-être le "est " gelé" chèque doit être dans le lecteur de méthode qui n'est pas l'auteur de la méthode (pour éviter de lecture avant d'être congelé).

Ou, si c'est le dernier, puis le cas d'utilisation que vous avez besoin de se méfier de l'est:

  • Thread principal invoque l'écrivain méthode, constate qu'il n'est pas gelé, et commence donc à écrire
  • Avant l'écriture est terminée, quelqu'un essaie de figer l'objet et le lit, tandis que l'autre (principale) de thread est encore écrit

Dans ce dernier cas, Google affiche beaucoup de résultats pour plusieurs lecteur seul écrivain qui pourraient vous intéresser.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X