web-dev-qa-db-fra.com

SemaphoreSlim (.NET) empêche-t-il le même thread d'entrer dans le bloc?

J'ai lu les documents pour SemaphoreSlim SemaphoreSlim MSDN qui indique que SemaphoreSlim limitera une section de code à exécuter par un seul thread à la fois si vous le configurez comme:

SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);

Cependant, il n'indique pas s'il empêche le même thread d'accéder à ce code. Cela arrive avec async et attend. Si l'on utilise l'attente dans une méthode, le contrôle quitte cette méthode et retourne lorsque la tâche ou le thread est terminé. Dans mon exemple, j'utilise un bouton avec un gestionnaire de bouton asynchrone. Il appelle une autre méthode (Function1) avec 'wait'. Function1 à son tour appelle

await Task.Run(() => Function2(beginCounter));

Autour de mon Task.Run () j'ai un SemaphoreSlim. Il semble que cela empêche le même thread d'accéder à Function2. Mais ce n'est pas garanti (comme je l'ai lu) de la documentation et je me demande si cela peut être compté.

J'ai posté mon exemple complet ci-dessous.

Merci,

Dave

 using System;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Windows;

 namespace AsynchAwaitExample
 {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
    public MainWindow()
    {
        InitializeComponent();
    }

    static int beginCounter = 0;
    static int endCounter = 0;
    /// <summary>
    /// Suggest hitting button 3 times in rapid succession
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private async void button_Click(object sender, RoutedEventArgs e)
    {
        beginCounter++;
        endCounter++;
        // Notice that if you click fast, you'll get all the beginCounters first, then the endCounters
        Console.WriteLine("beginCounter: " + beginCounter + " threadId: " + Thread.CurrentThread.ManagedThreadId);
        await Function1(beginCounter);
        Console.WriteLine("endCounter: " + endCounter + " threadId: " + Thread.CurrentThread.ManagedThreadId);
    }

    private async Task Function1(int beginCounter)
    {
        try
        {
            Console.WriteLine("about to grab lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
            await _semaphoreSlim.WaitAsync();  // get rid of _semaphoreSlim calls and you'll get into beginning of Function2 3 times before exiting
            Console.WriteLine("grabbed lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
            await Task.Run(() => Function2(beginCounter));
        }
        finally
        {
            Console.WriteLine("about to release lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
            _semaphoreSlim.Release();
            Console.WriteLine("released lock" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
        }

    }

    private void Function2(int beginCounter)
    {
        Console.WriteLine("Function2 start" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
        Thread.Sleep(1000);
        Console.WriteLine("Function2 end" + " threadId: " + Thread.CurrentThread.ManagedThreadId + " beginCounter: " + beginCounter);
        return;
    }
}
}

Exemple de sortie si vous cliquez 3 fois sur le bouton. Notez que Function2 se termine toujours pour un compteur donné avant de recommencer.

    beginCounter: 1 threadId: 9
about to grab lock threadId: 9 beginCounter: 1
grabbed lock threadId: 9 beginCounter: 1
Function2 start threadId: 13 beginCounter: 1
beginCounter: 2 threadId: 9
about to grab lock threadId: 9 beginCounter: 2
beginCounter: 3 threadId: 9
about to grab lock threadId: 9 beginCounter: 3
Function2 end threadId: 13 beginCounter: 1
about to release lock threadId: 9 beginCounter: 1
released lock threadId: 9 beginCounter: 1
grabbed lock threadId: 9 beginCounter: 2
Function2 start threadId: 13 beginCounter: 2
endCounter: 3 threadId: 9
Function2 end threadId: 13 beginCounter: 2
about to release lock threadId: 9 beginCounter: 2
released lock threadId: 9 beginCounter: 2
endCounter: 3 threadId: 9
grabbed lock threadId: 9 beginCounter: 3
Function2 start threadId: 13 beginCounter: 3
Function2 end threadId: 13 beginCounter: 3
about to release lock threadId: 9 beginCounter: 3
released lock threadId: 9 beginCounter: 3
endCounter: 3 threadId: 9

Si vous vous débarrassez des appels SemaphoreSlim, vous obtiendrez:

beginCounter: 1 threadId: 10
about to grab lock threadId: 10 beginCounter: 1
grabbed lock threadId: 10 beginCounter: 1
Function2 start threadId: 13 beginCounter: 1
beginCounter: 2 threadId: 10
about to grab lock threadId: 10 beginCounter: 2
grabbed lock threadId: 10 beginCounter: 2
Function2 start threadId: 14 beginCounter: 2
beginCounter: 3 threadId: 10
about to grab lock threadId: 10 beginCounter: 3
grabbed lock threadId: 10 beginCounter: 3
Function2 start threadId: 15 beginCounter: 3
Function2 end threadId: 13 beginCounter: 1
about to release lock threadId: 10 beginCounter: 1
released lock threadId: 10 beginCounter: 1
endCounter: 3 threadId: 10
Function2 end threadId: 14 beginCounter: 2
about to release lock threadId: 10 beginCounter: 2
released lock threadId: 10 beginCounter: 2
endCounter: 3 threadId: 10
18
Dave

De la documentation :

La classe SemaphoreSlim n'applique pas l'identité de thread ou de tâche lors des appels aux méthodes Wait, WaitAsync et Release

En d'autres termes, la classe ne cherche pas à savoir quel thread l'appelle. C'est juste un simple compteur. Le même thread peut acquérir le sémaphore plusieurs fois, et ce sera la même chose que si plusieurs threads ont acquis le sémaphore. Si le nombre de threads restants est descendu à 0, alors même si un thread était déjà celui qui avait acquis le sémaphore de ce thread, s'il appelle Wait(), il se bloquera jusqu'à ce qu'un autre thread libère le sémaphore.

Ainsi, en ce qui concerne async/await, le fait qu'un await puisse ou non reprendre dans le même thread où il a été lancé n'a pas d'importance. Tant que vous gardez vos appels Wait() et Release() équilibrés, cela fonctionnera comme on pourrait l'espérer.

Dans votre exemple, vous attendez même le sémaphore de manière asynchrone et ne bloquez donc aucun thread. Ce qui est bien, car sinon vous bloqueriez le thread d'interface utilisateur la deuxième fois que vous avez appuyé sur votre bouton.


Lecture connexe:
Verrouillage des ressources entre les itérations du thread principal (Async/Await)
Pourquoi ce code ne se termine-t-il pas dans une impasse
Verrouillage avec appels asynchrones imbriqués

Notez en particulier les mises en garde concernant le verrouillage rentrant/récursif, en particulier avec async/await. La synchronisation des threads est déjà assez délicate, et cette difficulté est ce que async/await est conçu pour simplifier. Et il le fait de manière significative dans la plupart des cas. Mais pas lorsque vous le mélangez avec un autre mécanisme de synchronisation/verrouillage.

19
Peter Duniho