41 votes

C'est le droit de thread-Safe?

Il suffit de vérifier ... _count est accessible en toute sécurité, non?

Les deux méthodes sont accessibles par plusieurs threads.

 private int _count;

public void CheckForWork() {
    if (_count >= MAXIMUM) return;
    Interlocked.Increment(ref _count);
    Task t = Task.Run(() => Work());
    t.ContinueWith(CompletedWorkHandler);
}

public void CompletedWorkHandler(Task completedTask) {
    Interlocked.Decrement(ref _count);
    // Handle errors, etc...
}
 

98voto

Eric Lippert Points 300275

C'est thread-safe, droit?

Supposons que le MAXIMUM est un compte est à zéro, et les cinq fils appel CheckForWork.

Tous les cinq fils n'a pas pu vérifier que le compteur est inférieur au MAXIMUM. Le compteur serait alors frappé jusqu'à cinq et cinq emplois allaient commencer.

Qui semble contraire à l'intention du code.

En outre: le champ n'est pas volatile. Donc, ce mécanisme garantit que n'importe quel thread va lire une valeur de date sur le pas-de mémoire-de la barrière de chemin? Rien ne garantit que! Vous faites seulement une barrière de mémoire si la condition est fausse.

Plus généralement: vous faites une fausse économie ici. En allant avec une faible solution de verrouillage vous permet d'économiser de la dizaine de nanosecondes que le uncontended de verrouillage pour le faire. Il suffit de prendre le verrou. Vous pouvez permettre l'extra dizaine de nanosecondes.

Et même plus généralement: ne pas écrire de basse-serrure à code, sauf si vous êtes un expert sur les architectures de processeur et de connaître toutes les optimisations que l'UC est autorisé à effectuer sur les bas-blocage des chemins. Vous n'êtes pas un tel expert. Je ne suis pas non plus. C'est pourquoi je n'écris pas de bas-code de verrouillage.

41voto

David S. Points 3239

Non, if (_count >= MAXIMUM) return; n'est pas thread-safe.

edit: Vous devez également verrouiller la lecture, qui doit ensuite être logiquement regroupée avec l'incrément, de sorte que je réécrive comme

 private int _count;

private readonly Object _locker_ = new Object();

public void CheckForWork() {
    lock(_locker_)
    {
        if (_count >= MAXIMUM)
            return;
        _count++;
    }
    Task.Run(() => Work());
}

public void CompletedWorkHandler() {
    lock(_locker_)
    {
        _count--;
    }
    ...
}
 

36voto

Jim Mischel Points 68586

C’est à quoi servent Semaphore et SemaphoreSlim :

 private readonly SemaphoreSlim WorkSem = new SemaphoreSlim(Maximum);

public void CheckForWork() {
    if (!WorkSem.Wait(0)) return;
    Task.Run(() => Work());
}

public void CompletedWorkHandler() {
    WorkSem.Release();
    ...
}
 

22voto

Brian Gideon Points 26683

Non, ce que vous avez n'est pas sûr. La vérification pour voir si _count >= MAXIMUM pourraient course avec l'appel à l' Interlocked.Increment partir d'un autre thread. C'est en fait vraiment très dur à résoudre à l'aide de basse-lock techniques. Pour obtenir que cela fonctionne correctement, vous devez avoir à faire une série de plusieurs opérations apparaissent atomique sans l'aide d'un verrou. C'est la partie la plus difficile. La série d'opérations en question ici sont:

  • Lire _count
  • Test _count >= MAXIMUM
  • Rendre une décision fondée sur ce qui précède.
  • Incrément _count selon la décision rendue.

Si vous ne faites pas tous les 4 de ces étapes apparaissent atomique puis il y aura une course à condition. Le modèle standard pour l'exécution d'une opération complexe, sans prendre un verrou est comme suit.

public static T InterlockedOperation<T>(ref T location)
{
  T initial, computed;
  do
  {
    initial = location;
    computed = op(initial); // where op() represents the operation
  } 
  while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
  return computed;
}

Remarquez ce qui se passe. L'opération est exécutée de façon répétée jusqu'à ce que l'ICX opération détermine que la valeur initiale n'a pas changé entre le moment où elle a été la première lecture et le temps de la tentative a été faite pour le changer. C'est le modèle standard et la magie, tout se passe en raison de l' CompareExchange (ICX) appel. Notez, cependant, que cela ne tienne pas compte de l' ABA problème.1

Ce que vous pourriez faire:

Donc, en prenant le dessus motif et l'incorporer dans votre code entraînerait dans cette.

public void CheckForWork() 
{
    int initial, computed;
    do
    {
      initial = _count;
      computed = initial < MAXIMUM ? initial + 1 : initial;
    }
    while (Interlocked.CompareExchange(ref _count, computed, initial) != initial);
    if (replacement > initial)
    {
      Task.Run(() => Work());
    }
}

Personnellement, je punt sur le bas-lock stratégie entièrement. Il y a plusieurs problèmes avec ce que j'ai présenté ci-dessus.

  • Cela pourrait effectivement fonctionner plus lentement que de prendre un hard lock. Les raisons sont difficiles à expliquer et à l'extérieur de la portée de ma réponse.
  • Toute dérogation à ce qui est au-dessus va probablement provoquer le code à l'échec. Oui, c'est vraiment fragile.
  • C'est difficile à comprendre. Je veux dire regarder. Qu'il est laid.

Ce que vous devez faire:

Aller avec le hard lock route votre code pourrait ressembler à ceci.

private object _lock = new object();
private int _count;

public void CheckForWork() 
{
  lock (_lock)
  {
    if (_count >= MAXIMUM) return;
    _count++;
  }
  Task.Run(() => Work());
}

public void CompletedWorkHandler() 
{
  lock (_lock)
  {
    _count--;
  }
}

Remarquez que c'est beaucoup plus simple et beaucoup moins sujettes à erreur. Vous pouvez effectivement trouver que cette approche (hard lock) est effectivement plus rapide que ce que j'ai montré ci-dessus (bas de verrouillage). Encore une fois, la raison en est délicate et il existe des techniques qui peuvent être utilisés pour accélérer les choses, mais en dehors de la portée de cette réponse.


1L'ABA problème n'est pas vraiment un problème dans ce cas, car la logique ne dépend pas de l' _count restant inchangée. Il importe seulement que sa valeur est la même à deux points dans le temps, indépendamment de ce qui s'est passé entre les deux. En d'autres termes, le problème peut être réduit à celui dans lequel il semblait comme la valeur n'a pas changé, même si, en réalité, il peut avoir.

4voto

Kris Vandermotten Points 5187

Définir le thread safe.

Si vous voulez vous assurer que _count ne sera jamais supérieur à MAXIMUM, vous n’avez pas réussi.

Ce que vous devriez faire, c'est aussi:

 private int _count;
private object locker = new object();

public void CheckForWork() 
{
    lock(locker)
    {
        if (_count >= MAXIMUM) return;
        _count++;
    }
    Task.Run(() => Work());
}

public void CompletedWorkHandler() 
{
    lock(locker)
    {
        _count--;
    }
    ...
}
 

Vous voudrez peut-être aussi jeter un coup d'œil à la classe SemaphoreSlim .

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