Besoin de comprendre l'utilisation de SemaphoreSlim

90

Voici le code que j'ai mais je ne comprends pas ce que je SemaphoreSlimfais.

async Task WorkerMainAsync()
{
    SemaphoreSlim ss = new SemaphoreSlim(10);
    List<Task> trackedTasks = new List<Task>();
    while (DoMore())
    {
        await ss.WaitAsync();
        trackedTasks.Add(Task.Run(() =>
        {
            DoPollingThenWorkAsync();
            ss.Release();
        }));
    }
    await Task.WhenAll(trackedTasks);
}

void DoPollingThenWorkAsync()
{
    var msg = Poll();
    if (msg != null)
    {
        Thread.Sleep(2000); // process the long running CPU-bound job
    }
}

Qu'est-ce qui attend ss.WaitAsync();et que ss.Release();fait?

Je suppose que si j'exécute 50 threads à la fois, j'écris du code comme SemaphoreSlim ss = new SemaphoreSlim(10);il sera alors obligé d'exécuter 10 threads actifs à la fois.

Lorsque l'un des 10 threads se termine, un autre thread démarre. Si je n'ai pas raison, aidez-moi à comprendre avec un exemple de situation.

Pourquoi est-il awaitnécessaire avec ss.WaitAsync();? Que fait ss.WaitAsync();-on?

Mou
la source
3
Une chose à noter est que vous devriez vraiment encapsuler ce "DoPollingThenWorkAsync ();" dans un "try {DoPollingThenWorkAsync ();} finally {ss.Release ();}", sinon les exceptions affameront définitivement ce sémaphore.
Austin Salgat le
Je me sens un peu étrange que nous acquérons et libérons le sémaphore respectivement à l'extérieur / à l'intérieur de la tâche. Le déplacement de "await ss.WaitAsync ()" dans la tâche fera-t-il une différence?
Shane Lu

Réponses:

73

je suppose que si je lance 50 threads à la fois, code comme SemaphoreSlim ss = new SemaphoreSlim (10); forcera à exécuter 10 threads actifs à la fois

C'est correct; l'utilisation du sémaphore garantit qu'il n'y aura pas plus de 10 ouvriers effectuant ce travail en même temps.

L'appel WaitAsyncsur le sémaphore produit une tâche qui sera achevée lorsque ce thread aura "accès" à ce jeton. await-ing cette tâche permet au programme de continuer son exécution quand il est "autorisé" à le faire. Avoir une version asynchrone, plutôt que d'appeler Wait, est important à la fois pour s'assurer que la méthode reste asynchrone, plutôt que synchrone, ainsi que pour gérer le fait qu'une asyncméthode peut exécuter du code sur plusieurs threads, en raison des rappels, etc. l'affinité naturelle du fil avec les sémaphores peut être un problème.

Remarque: DoPollingThenWorkAsyncne devrait pas avoir le Asyncsuffixe car il n'est pas réellement asynchrone, il est synchrone. Appelez ça DoPollingThenWork. Cela réduira la confusion pour les lecteurs.

Servy
la source
merci mais s'il vous plaît dites-moi ce qui se passe lorsque nous spécifions pas de thread à exécuter, disons 10.Lorsque l'un des 10 threads se termine, ce thread saute à nouveau pour terminer un autre travail ou retourne au pool? ce n'est pas très clair pour ... alors veuillez expliquer ce qui se passe derrière la scène.
Mou
@Mou Qu'est-ce qui n'est pas clair? Le code attend jusqu'à ce qu'il y ait moins de 10 tâches en cours d'exécution; quand il y en a, il en ajoute un autre. Lorsqu'une tâche se termine, cela indique qu'elle est terminée. C'est ça.
Servy
quel est l'avantage de ne spécifier aucun thread à exécuter. si trop de threads peuvent nuire aux performances? si oui, pourquoi entraver ... si je lance 50 threads au lieu de 10 threads, alors pourquoi les performances seront importantes ... pouvez-vous expliquer. merci
Thomas
4
@Thomas Si vous avez trop de threads simultanés, les threads passent plus de temps à changer de contexte qu'ils n'en consacrent à un travail productif. Le débit diminue à mesure que les threads augmentent, car vous passez de plus en plus de temps à gérer les threads au lieu de travailler, du moins une fois que votre nombre de threads dépasse de beaucoup le nombre de cœurs sur la machine.
Servy
3
@Servy Cela fait partie du travail du planificateur de tâches. Tâches! = Threads. Le Thread.Sleepdans le code d'origine dévasterait le planificateur de tâches. Si vous n'êtes pas asynchrone au cœur, vous n'êtes pas asynchrone.
Joseph Lennox
54

Dans la maternelle du coin, ils utilisent un SemaphoreSlim pour contrôler le nombre d'enfants qui peuvent jouer dans la salle de PE.

Ils ont peint sur le sol, à l'extérieur de la pièce, 5 paires d'empreintes de pas.

À l'arrivée des enfants, ils laissent leurs chaussures sur une paire d'empreintes libres et entrent dans la pièce.

Une fois qu'ils ont fini de jouer, ils sortent, récupèrent leurs chaussures et «libèrent» une place pour un autre enfant.

Si un enfant arrive et qu'il n'y a plus d'empreintes de pas, il va jouer ailleurs ou simplement rester un moment et vérifier de temps en temps (c'est-à-dire pas de priorités FIFO).

Lorsqu'un enseignant est là, elle «libère» une rangée supplémentaire de 5 empreintes de pas de l'autre côté du couloir de sorte que 5 autres enfants puissent jouer dans la pièce en même temps.

Il a aussi les mêmes «pièges» que SemaphoreSlim ...

Si un enfant finit de jouer et quitte la pièce sans ramasser les chaussures (ne déclenche pas le «release») alors la fente reste bloquée, même s'il y a théoriquement une fente vide. Cependant, l'enfant se fait généralement dire.

Parfois, un ou deux enfants sournois cachent leurs chaussures ailleurs et entrent dans la pièce, même si toutes les empreintes de pas sont déjà prises (c'est-à-dire que le SemaphoreSlim ne contrôle pas "vraiment" le nombre d'enfants dans la pièce).

Cela ne se termine généralement pas bien, car la surpopulation de la salle a tendance à se terminer par les pleurs des enfants et par le professeur qui ferme complètement la salle.

Dandiez
la source
3
Ces types de réponses sont mes préférés.
Ma pile déborde le
OMG c'est informatif et drôle comme diable!
Zonus
7

Bien que j'accepte cette question se rapporte vraiment à un scénario de verrouillage de compte à rebours, j'ai pensé qu'il valait la peine de partager ce lien que j'ai découvert pour ceux qui souhaitent utiliser un SemaphoreSlim comme un simple verrou asynchrone. Il vous permet d'utiliser l'instruction using qui pourrait rendre le codage plus propre et plus sûr.

http://www.tomdupont.net/2016/03/how-to-release-semaphore-with-using.html

J'ai échangé _isDisposed=trueet _semaphore.Release()circulé dans son Dispose au cas où il serait appelé plusieurs fois.

Il est également important de noter que SemaphoreSlim n'est pas un verrou réentrant, ce qui signifie que si le même thread appelle WaitAsync plusieurs fois, le nombre du sémaphore est décrémenté à chaque fois. En bref, SemaphoreSlim n'est pas compatible avec Thread.

En ce qui concerne les questions sur la qualité du code, il est préférable de placer la version dans le dernier essai pour s'assurer qu'elle est toujours publiée.

andrew pate
la source
6
Il est déconseillé de publier des réponses contenant uniquement des liens, car les liens ont tendance à s'éteindre avec le temps, rendant ainsi la réponse sans valeur. Si vous le pouvez, il est préférable de résumer les points clés ou le bloc de code clé dans votre réponse.
John