Ceci est Thread-Safe non?

_count simplement que _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... } 

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

edit: Il faudrait aussi verrouiller la lecture, qui devrait ensuite être logiquement groupée avec l’incrément, donc je réécrirais 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--; } ... } 

Ceci est thread-safe, non?

Supposons que MAXIMUM soit un, que count est égal à zéro et que cinq threads appellent CheckForWork.

Les cinq threads peuvent vérifier que le nombre est inférieur à MAXIMUM. Le comptoir serait alors remplacé par cinq et cinq emplois commenceraient.

Cela semble contraire à l’intention du code.

De plus, le champ n’est pas volatile. Alors, quel mécanisme garantit que n’importe quel thread lira une valeur à jour sur le chemin no-memory-barrier? Rien ne le garantit! Vous ne créez une barrière de mémoire que si la condition est fausse.

Plus généralement, vous faites une fausse économie ici. En optant pour une solution à faible encombrement, vous économisez la douzaine de nanosecondes que le verrou non forcé prendrait. Il suffit de prendre la serrure . Vous pouvez vous permettre la douzaine de nanosecondes supplémentaires.

Et plus généralement: n’écrivez pas de code à faible locking, à moins d’être un expert des architectures de processeurs et de connaître toutes les optimisations qu’un processeur est autorisé à effectuer sur les chemins à faible locking . Vous n’êtes pas un tel expert. Je ne suis pas non plus. C’est pourquoi je n’écris pas de code à faible locking .

C’est pour ça que Semaphore et SemaphoreSlim sont:

 private readonly SemaphoreSlim WorkSem = new SemaphoreSlim(Maximum); public void CheckForWork() { if (!WorkSem.Wait(0)) return; Task.Run(() => Work()); } public void CompletedWorkHandler() { WorkSem.Release(); ... } 

Non, ce que vous avez n’est pas sûr. La vérification pour voir si _count >= MAXIMUM pourrait courir avec l’appel à Interlocked.Increment d’un autre thread. C’est en fait très difficile à résoudre en utilisant des techniques à faible locking. Pour que cela fonctionne correctement, vous devez faire apparaître plusieurs séries d’opérations sans utiliser de verrou. C’est la partie difficile. Les séries d’opérations en question ici sont:

  • Lire _count
  • Test _count >= MAXIMUM
  • Prendre une décision basée sur ce qui précède.
  • Incrémenter _count fonction de la décision prise.

Si vous ne faites pas apparaître les quatre étapes atomiques, il y aura une situation de concurrence. Le modèle standard pour effectuer une opération complexe sans effectuer de locking est le suivant.

 public static T InterlockedOperation(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; } 

Notez ce qui se passe. L’opération est répétée jusqu’à ce que l’opération ICX détermine que la valeur initiale n’a pas changé entre le moment où elle a été lue pour la première fois et le moment où la tentative de modification a été effectuée. Ceci est le modèle standard et la magie se produit à cause de l’ CompareExchange (ICX). Notez cependant que cela ne prend pas en compte le problème ABA . 1

Ce que vous pourriez faire:

Donc, en prenant le modèle ci-dessus et en l’intégrant dans votre code, vous obtenez ceci.

 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 miserais entièrement sur la stratégie de locking bas. Il y a plusieurs problèmes avec ce que j’ai présenté ci-dessus.

  • Cela peut en fait être plus lent que de prendre un verrou dur. Les raisons sont difficiles à expliquer et échappent à la scope de ma réponse.
  • Tout écart par rapport à ce qui précède entraînera probablement l’échec du code. Oui, c’est vraiment fragile.
  • C’est difficile à comprendre. Je veux dire, regarde-le. C’est moche.

Ce que vous devriez faire:

En utilisant la route de locking dur, 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--; } } 

Notez que cela est beaucoup plus simple et considérablement moins sujet aux erreurs. Vous pouvez en fait constater que cette approche (locking matériel) est en réalité plus rapide que ce que j’ai montré ci-dessus (locking bas). Encore une fois, la raison est délicate et il existe des techniques qui peuvent être utilisées pour accélérer les choses, mais cela dépasse le cadre de cette réponse.


1 Le problème ABA n’est pas vraiment un problème dans ce cas car la logique ne dépend pas du _count restant inchangé. Il importe seulement que sa valeur soit la même à deux moments différents, indépendamment de ce qui s’est passé entre les deux. En d’autres termes, le problème peut être réduit à un problème dans lequel il semble que la valeur n’a pas changé, même si, en réalité, elle peut avoir changé.

Définir le fil sûr.

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

Ce que vous devez faire est de verrouiller cela 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 pourriez également vouloir jeter un oeil à la classe SemaphoreSlim .

vous pouvez faire ce qui suit si vous ne voulez pas verrouiller ou déplacer un sémaphore:

 if (_count >= MAXIMUM) return; // not necessary but handy as early return if(Interlocked.Increment(ref _count)>=MAXIMUM+1) { Interlocked.Decrement(ref _count);//restore old value return; } Task.Run(() => Work()); 

Increment renvoie la valeur incrémentée sur laquelle vous pouvez vérifier si _count est inférieur au maximum, si le test échoue, je restaure l’ancienne valeur