Pourquoi ne puis-je pas utiliser l'opérateur 'wait' dans le corps d'une instruction de verrouillage?

348

Le mot clé wait en C # (.NET Async CTP) n'est pas autorisé à partir d'une instruction de verrouillage.

Depuis MSDN :

Une expression d'attente ne peut pas être utilisée dans une fonction synchrone, dans une expression de requête, dans le bloc catch ou finally d'une instruction de gestion des exceptions, dans le bloc d'une instruction lock ou dans un contexte non sécurisé.

Je suppose que cela est difficile ou impossible pour l'équipe de compilation à implémenter pour une raison quelconque.

J'ai tenté une solution de contournement avec l'instruction using:

class Async
{
    public static async Task<IDisposable> Lock(object obj)
    {
        while (!Monitor.TryEnter(obj))
            await TaskEx.Yield();

        return new ExitDisposable(obj);
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object obj;
        public ExitDisposable(object obj) { this.obj = obj; }
        public void Dispose() { Monitor.Exit(this.obj); }
    }
}

// example usage
using (await Async.Lock(padlock))
{
    await SomethingAsync();
}

Cependant, cela ne fonctionne pas comme prévu. L'appel à Monitor.Exit dans ExitDisposable.Dispose semble se bloquer indéfiniment (la plupart du temps) provoquant des blocages lorsque d'autres threads tentent d'acquérir le verrou. Je soupçonne le manque de fiabilité de mon travail et la raison pour laquelle les déclarations d'attente ne sont pas autorisées dans la déclaration de verrouillage sont en quelque sorte liées.

Quelqu'un sait-il pourquoi l' attente n'est pas autorisée dans le corps d'une instruction de verrouillage?

Kevin
la source
27
J'imagine que vous avez trouvé la raison pour laquelle ce n'est pas autorisé.
asawyer
Je commence juste à rattraper mon retard et à en apprendre un peu plus sur la programmation asynchrone. Après de nombreux blocages dans mes applications wpf, j'ai trouvé que cet article était un excellent garde-fou dans les pratiques de programmation asynchrone. msdn.microsoft.com/en-us/magazine/…
C. Tewalt
Le verrou est conçu pour empêcher l'accès asynchrone lorsque l'accès asynchrone casserait votre code, ergo si vous utilisez async à l'intérieur d'un verrou, vous avez invalidé votre verrou .. donc si vous devez attendre quelque chose à l'intérieur de votre verrou, vous n'utilisez pas le verrou correctement
MikeT

Réponses:

366

Je suppose que cela est difficile ou impossible pour l'équipe de compilation à implémenter pour une raison quelconque.

Non, ce n'est pas du tout difficile ou impossible à mettre en œuvre - le fait que vous l'ayez mis en œuvre vous-même en témoigne. C'est plutôt une idée incroyablement mauvaise et nous ne la permettons pas, afin de vous protéger contre cette erreur.

L'appel à Monitor.Exit dans ExitDisposable.Dispose semble se bloquer indéfiniment (la plupart du temps) provoquant des blocages lorsque d'autres threads tentent d'acquérir le verrou. Je soupçonne le manque de fiabilité de mon travail et la raison pour laquelle les déclarations d'attente ne sont pas autorisées dans la déclaration de verrouillage sont en quelque sorte liées.

C'est exact, vous avez découvert pourquoi nous l'avons rendu illégal. L'attente à l'intérieur d'une serrure est une recette pour produire des blocages.

Je suis sûr que vous pouvez voir pourquoi: du code arbitraire s'exécute entre le moment où le contrôle de retour en attente à l'appelant et la méthode reprend . Ce code arbitraire pourrait supprimer des verrous qui produisent des inversions d'ordre de verrouillage, et donc des blocages.

Pire, le code pourrait reprendre sur un autre thread (dans les scénarios avancés; normalement, vous reprenez sur le thread qui a attendu, mais pas nécessairement), auquel cas le déverrouillage déverrouillerait un verrou sur un thread différent de celui qui a pris la serrure. est-ce une bonne idée? Non.

Je note que c'est aussi une "pire pratique" de faire un yield returnintérieur d'un lock, pour la même raison. Il est légal de le faire, mais j'aurais souhaité que nous l'ayons rendu illégal. Nous n'allons pas faire la même erreur pour "attendre".

Eric Lippert
la source
189
Comment gérez-vous un scénario dans lequel vous devez renvoyer une entrée de cache, et si l'entrée n'existe pas, vous devez calculer le contenu de manière asynchrone, puis ajouter + renvoyer l'entrée, en vous assurant que personne d'autre ne vous appelle entre-temps?
Softlion
9
Je me rends compte que je suis en retard à la fête ici, mais j'ai été surpris de voir que vous mettez des blocages comme principale raison pour laquelle c'est une mauvaise idée. J'étais arrivé à la conclusion dans ma propre pensée que la nature rentrante de la serrure / moniteur serait une plus grande partie du problème. Autrement dit, vous mettez en file d'attente deux tâches dans le pool de threads lock (), qui dans un monde synchrone s'exécuteraient sur des threads séparés. Mais maintenant, avec wait (si cela est autorisé, je veux dire), vous pouvez avoir deux tâches à exécuter dans le bloc de verrouillage car le thread a été réutilisé. L'hilarité s'ensuit. Ou ai-je mal compris quelque chose?
Gareth Wilson
4
@GarethWilson: J'ai parlé de blocages car la question posée portait sur les blocages . Vous avez raison: des problèmes de rentrée bizarres sont possibles et semblent probables.
Eric Lippert
11
@Eric Lippert. Étant donné que la SemaphoreSlim.WaitAsyncclasse a été ajoutée au framework .NET bien après la publication de cette réponse, je pense que nous pouvons supposer en toute sécurité que c'est possible maintenant. Quoi qu'il en soit, vos commentaires sur la difficulté de mettre en œuvre une telle construction sont toujours entièrement valables.
Contango
7
"du code arbitraire s'exécute entre le moment où l'attente renvoie le contrôle à l'appelant et la méthode reprend" - cela est sûrement vrai de tout code, même en l'absence d'async / wait, dans un contexte multithread: d'autres threads peuvent exécuter du code arbitraire à n'importe quel temps, et dit que le code arbitraire comme vous le dites "pourrait être en train de supprimer des verrous qui produisent des inversions d'ordre de verrouillage, et donc des interblocages." Alors, pourquoi est-ce particulièrement important avec async / wait? Je comprends que le deuxième point concernant "le code pourrait reprendre sur un autre fil" est particulièrement important pour async / wait.
bacar
291

Utilisez la SemaphoreSlim.WaitAsyncméthode.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
user1639030
la source
10
Comme cette méthode a été introduite récemment dans le framework .NET, je pense que nous pouvons supposer que le concept de verrouillage dans un monde asynchrone / attente est maintenant bien éprouvé.
Contango
5
Pour plus d'informations, recherchez le texte "SemaphoreSlim" dans cet article: Async / Await - Meilleures pratiques en programmation asynchrone
BobbyA
1
@JamesKo si toutes ces tâches attendent le résultat de Stuffje ne vois aucun moyen de le contourner ...
Ohad Schneider
7
Ne devrait-il pas être initialisé comme mySemaphoreSlim = new SemaphoreSlim(1, 1)pour fonctionner comme lock(...)?
Sergey
3
Ajout de la version étendue de cette réponse: stackoverflow.com/a/50139704/1844247
Sergey
67

Ce serait fondamentalement la mauvaise chose à faire.

Il y a deux façons cela pourrait être mis en œuvre:

  • Maintenez la serrure, ne la relâchez qu'à la fin du bloc .
    C'est une très mauvaise idée car vous ne savez pas combien de temps l'opération asynchrone va prendre. Vous ne devez tenir les verrous que pendant une durée minimale . C'est également potentiellement impossible, car un thread possède un verrou, pas une méthode - et vous ne pouvez même pas exécuter le reste de la méthode asynchrone sur le même thread (selon le planificateur de tâches).

  • Relâchez le verrou dans l'attente et réacquérez-le lorsque l'attente revient
    Cela viole le principe de l'OMI le moins étonnant, où la méthode asynchrone doit se comporter aussi étroitement que possible comme le code synchrone équivalent - sauf si vous utilisez Monitor.Waitdans un bloc de verrouillage, vous vous attendez à posséder la serrure pour la durée du bloc.

Donc, fondamentalement, il y a deux exigences concurrentes ici - vous ne devriez pas essayer de faire la première ici, et si vous voulez adopter la deuxième approche, vous pouvez rendre le code beaucoup plus clair en ayant deux blocs de verrouillage séparés séparés par l'expression d'attente:

// Now it's clear where the locks will be acquired and released
lock (foo)
{
}
var result = await something;
lock (foo)
{
}

Ainsi, en vous interdisant d'attendre dans le bloc de verrouillage lui-même, le langage vous oblige à réfléchir à ce que vous voulez vraiment faire et à rendre ce choix plus clair dans le code que vous écrivez.

Jon Skeet
la source
5
Étant donné que la SemaphoreSlim.WaitAsyncclasse a été ajoutée au framework .NET bien après la publication de cette réponse, je pense que nous pouvons supposer en toute sécurité que c'est possible maintenant. Quoi qu'il en soit, vos commentaires sur la difficulté de mettre en œuvre une telle construction sont toujours entièrement valables.
Contango
7
@Contango: Eh bien, ce n'est pas tout à fait la même chose. En particulier, le sémaphore n'est pas lié à un thread spécifique. Il atteint des objectifs similaires à verrouiller, mais il existe des différences importantes.
Jon Skeet
@JonSkeet je sais que c'est un vieux fil très complet et tout, mais je ne sais pas comment l'appel à quelque chose () est protégé en utilisant ces verrous de la deuxième manière? lorsqu'un thread exécute quelque chose (), n'importe quel autre thread peut également y participer! Est-ce que j'ai râté quelque chose ?
@Joseph: Ce n'est pas protégé à ce stade. C'est la deuxième approche, qui indique clairement que vous acquérez / libérez, puis acquérez / relâchez à nouveau, peut-être sur un thread différent. Parce que la première approche est une mauvaise idée, selon la réponse d'Eric.
Jon Skeet
41

Ce n'est qu'une extension de cette réponse .

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

public class SemaphoreLocker
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task LockAsync(Func<Task> worker)
    {
        await _semaphore.WaitAsync();
        try
        {
            await worker();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

Usage:

public class Test
{
    private static readonly SemaphoreLocker _locker = new SemaphoreLocker();

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [asyn] calls can be used within this block 
            // to handle a resource by one thread. 
        });
    }
}
Sergey
la source
1
Il peut être dangereux d'obtenir le verrouillage du sémaphore en dehors du trybloc - si une exception se produit entre WaitAsyncet que tryle sémaphore ne sera jamais libéré (blocage). D'un autre côté, le déplacement de l' WaitAsyncappel dans le trybloc introduira un autre problème, lorsque le sémaphore peut être libéré sans qu'un verrou ne soit acquis. Voir le fil associé où ce problème a été expliqué: stackoverflow.com/a/61806749/7889645
AndreyCh
16

Cela fait référence à http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , Windows 8 app store et .net 4.5

Voici mon point de vue à ce sujet:

La fonctionnalité de langue asynchrone / attente rend beaucoup de choses assez faciles, mais elle introduit également un scénario qui était rarement rencontré avant qu'il ne soit si facile d'utiliser des appels asynchrones: la réentrance.

Cela est particulièrement vrai pour les gestionnaires d'événements, car pour de nombreux événements, vous n'avez aucune idée de ce qui se passe après votre retour du gestionnaire d'événements. Une chose qui pourrait réellement se produire est que la méthode asynchrone que vous attendez dans le premier gestionnaire d'événements est appelée à partir d'un autre gestionnaire d'événements toujours sur le même thread.

Voici un scénario réel que j'ai rencontré dans une application Windows 8 App Store: Mon application a deux cadres: entrant et sortant d'un cadre, je veux charger / sauvegarder certaines données dans un fichier / stockage. Les événements OnNavigatedTo / From sont utilisés pour l'enregistrement et le chargement. L'enregistrement et le chargement sont effectués par une fonction utilitaire asynchrone (comme http://winrtstoragehelper.codeplex.com/ ). Lorsque vous naviguez de la trame 1 à la trame 2 ou dans l'autre sens, la charge asynchrone et les opérations sûres sont appelées et attendues. Les gestionnaires d'événements deviennent asynchrones et renvoient void => ils ne peuvent pas être attendus.

Cependant, la première opération d'ouverture de fichier (disons: à l'intérieur d'une fonction de sauvegarde) de l'utilitaire est également asynchrone et donc la première attente retourne le contrôle sur le framework, qui appellera parfois l'autre utilitaire (charge) via le deuxième gestionnaire d'événements. Le chargement essaie maintenant d'ouvrir le même fichier et si le fichier est désormais ouvert pour l'opération de sauvegarde, échoue avec une exception ACCESSDENIED.

Une solution minimale pour moi est de sécuriser l'accès au fichier via un using et un AsyncLock.

private static readonly AsyncLock m_lock = new AsyncLock();
...

using (await m_lock.LockAsync())
{
    file = await folder.GetFileAsync(fileName);
    IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read);
    using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result)
    {
        return (T)serializer.Deserialize(inStream);
    }
}

Veuillez noter que son verrou verrouille essentiellement toutes les opérations sur les fichiers pour l'utilitaire avec un seul verrou, ce qui est inutilement fort mais fonctionne bien pour mon scénario.

Voici mon projet de test: une application Windows 8 App Store avec quelques appels de test pour la version originale de http://winrtstoragehelper.codeplex.com/ et ma version modifiée qui utilise le AsyncLock de Stephen Toub http: //blogs.msdn. com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx .

Puis-je également suggérer ce lien: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

Hans
la source
7

Stephen Taub a implémenté une solution à cette question, voir Création de primitives de coordination asynchrone, partie 7: AsyncReaderWriterLock .

Stephen Taub est très apprécié dans l'industrie, donc tout ce qu'il écrit est susceptible d'être solide.

Je ne reproduirai pas le code qu'il a posté sur son blog, mais je vais vous montrer comment l'utiliser:

/// <summary>
///     Demo class for reader/writer lock that supports async/await.
///     For source, see Stephen Taub's brilliant article, "Building Async Coordination
///     Primitives, Part 7: AsyncReaderWriterLock".
/// </summary>
public class AsyncReaderWriterLockDemo
{
    private readonly IAsyncReaderWriterLock _lock = new AsyncReaderWriterLock(); 

    public async void DemoCode()
    {           
        using(var releaser = await _lock.ReaderLockAsync()) 
        { 
            // Insert reads here.
            // Multiple readers can access the lock simultaneously.
        }

        using (var releaser = await _lock.WriterLockAsync())
        {
            // Insert writes here.
            // If a writer is in progress, then readers are blocked.
        }
    }
}

Si vous voulez une méthode intégrée au framework .NET, utilisez SemaphoreSlim.WaitAsyncplutôt. Vous n'obtiendrez pas de verrou de lecture / écriture, mais vous obtiendrez une implémentation éprouvée.

Contango
la source
Je suis curieux de savoir s'il existe des réserves à l'utilisation de ce code. Si quelqu'un peut démontrer des problèmes avec ce code, j'aimerais le savoir. Cependant, ce qui est vrai, c'est que le concept de verrouillage asynchrone / attente est définitivement bien éprouvé, comme SemaphoreSlim.WaitAsyncc'est le cas dans le cadre .NET. Tout ce code ne fait qu'ajouter un concept de verrouillage de lecteur / écrivain.
Contango
3

Hmm, l'air moche, semble fonctionner.

static class Async
{
    public static Task<IDisposable> Lock(object obj)
    {
        return TaskEx.Run(() =>
            {
                var resetEvent = ResetEventFor(obj);

                resetEvent.WaitOne();
                resetEvent.Reset();

                return new ExitDisposable(obj) as IDisposable;
            });
    }

    private static readonly IDictionary<object, WeakReference> ResetEventMap =
        new Dictionary<object, WeakReference>();

    private static ManualResetEvent ResetEventFor(object @lock)
    {
        if (!ResetEventMap.ContainsKey(@lock) ||
            !ResetEventMap[@lock].IsAlive)
        {
            ResetEventMap[@lock] =
                new WeakReference(new ManualResetEvent(true));
        }

        return ResetEventMap[@lock].Target as ManualResetEvent;
    }

    private static void CleanUp()
    {
        ResetEventMap.Where(kv => !kv.Value.IsAlive)
                     .ToList()
                     .ForEach(kv => ResetEventMap.Remove(kv));
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object _lock;

        public ExitDisposable(object @lock)
        {
            _lock = @lock;
        }

        public void Dispose()
        {
            ResetEventFor(_lock).Set();
        }

        ~ExitDisposable()
        {
            CleanUp();
        }
    }
}
Anton Pogonets
la source
0

J'ai essayé d'utiliser un moniteur (code ci-dessous) qui semble fonctionner mais qui a un GOTCHA ... lorsque vous avez plusieurs threads, il donnera ... System.Threading.SynchronizationLockException La méthode de synchronisation d'objet a été appelée à partir d'un bloc de code non synchronisé.

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

namespace MyNamespace
{
    public class ThreadsafeFooModifier : 
    {
        private readonly object _lockObject;

        public async Task<FooResponse> ModifyFooAsync()
        {
            FooResponse result;
            Monitor.Enter(_lockObject);
            try
            {
                result = await SomeFunctionToModifyFooAsync();
            }
            finally
            {
                Monitor.Exit(_lockObject);
            }
            return result;
        }
    }
}

Avant cela, je faisais simplement cela, mais c'était dans un contrôleur ASP.NET, ce qui a entraîné un blocage.

public async Task<FooResponse> ModifyFooAsync() { lock(lockObject) { return SomeFunctionToModifyFooAsync.Result; } }

Andrew Pate
la source