web-dev-qa-db-fra.com

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

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

De MSDN :

Une expression wait 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 de verrouillage ou dans un contexte non sécurisé.

Je suppose que cela est difficile ou impossible pour l’équipe de compilation à mettre en œuvre pour une raison quelconque.

J'ai tenté de contourner le problème avec la déclaration 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 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 en attente ne sont pas autorisées dans la déclaration de verrouillage sont en quelque sorte liées.

Est-ce que quelqu'un sait pourquoi attendre n'est pas autorisé dans le corps d'une instruction de verrouillage?

304
Kevin

Je suppose que cela est difficile ou impossible pour l’équipe de compilation à mettre en œuvre 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. Au contraire, c'est une très mauvaise idée et donc nous ne le permettons pas, afin de vous protéger de cette erreur.

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

Correct, vous avez découvert pourquoi nous l'avons rendu illégal. Attendre dans une serrure est une recette permettant de produire des blocages.

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

Pire, le code pourrait reprendre sur un autre thread (dans des scénarios avancés; normalement, vous récupérez le thread qui a attendu, mais pas nécessairement) dans Dans ce cas, le déverrouillage déverrouillerait un verrou sur un autre thread que celui qui a retiré le verrou. est-ce une bonne idée? Non.

Je remarque que c’est aussi une "mauvaise pratique" de faire un yield return à l’intérieur d’un lock, pour la même raison. C'est légal, mais j'aurais aimé que ce soit illégal. Nous n'allons pas faire la même erreur pour "attendre".

328
Eric Lippert

Utilisez la méthode SemaphoreSlim.WaitAsync .

_ await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
_
246
user1639030

Fondamentalement, ce ne serait pas la bonne chose à faire.

Il y a deux manières de mettre en œuvre :

  • Tenez la serrure en main, ne la libérant 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 conserver les verrous que pour minimum . C'est aussi 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).

  • Libère le verrou dans l'attente et le réacquiert lorsque l'attente revient
    Ceci viole le principe de moindre étonnement IMO, selon lequel la méthode asynchrone devrait se comporter aussi fidèlement que possible comme le code synchrone équivalent - à moins que vous utilisiez Monitor.Wait dans un bloc de verrouillage, vous vous attendez à posséder le verrou pour la durée du bloc.

Donc, fondamentalement, il y a deux exigences en concurrence 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 wait:

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

Donc, en vous interdisant d’attendre dans le bloc de verrouillage lui-même, le langage vous oblige à penser à ce que vous voulez vraiment et rendre ce choix plus clair. dans le code que vous écrivez.

65
Jon Skeet

Ceci est juste 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();
        }
    }
}

Utilisation:

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. 
        });
    }
}
21
Sergey

Ceci 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 angle sur ceci:

La fonctionnalité async/wait language facilite beaucoup de choses, mais elle introduit également un scénario rarement rencontré avant la facilité d'utilisation des appels asynchrones: la réentrance.

Cela est particulièrement vrai pour les gestionnaires d'événements, car pour de nombreux événements, vous ne savez pas ce qui se passe après votre retour du gestionnaire d'événements. Une chose qui peut réellement arriver 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 que j'ai rencontré dans une application Windows 8 App Store: Mon application comporte deux cadres: entrer et sortir d'un cadre, je veux charger/sauvegarder des données dans un fichier/stockage. Les événements OnNavigatedTo/From sont utilisés pour la sauvegarde et le chargement. La sauvegarde et le chargement sont effectués par une fonction d’utilitaire asynchrone (comme http://winrtstoragehelper.codeplex.com/ ). Lorsque vous naviguez d'une image 1 à une image 2 ou dans l'autre sens, les opérations de chargement asynchrone et de sécurité sont appelées et attendues. Les gestionnaires d'événements deviennent asynchrones et reviennent à vide => 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. Par conséquent, la première attente renvoie le contrôle au framework, qui appelle plus tard l'autre utilitaire (load) via le second gestionnaire d'événements. Le chargement tente maintenant d'ouvrir le même fichier et si le fichier est déjà ouvert pour l'opération de sauvegarde, échoue avec une exception ACCESSDENIED.

Une solution minimale pour moi est de sécuriser l’accès aux fichiers 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 fondamentalement toutes les opérations de fichiers de l'utilitaire avec un seul verrou, ce qui est inutilement fort mais fonctionne bien pour mon scénario.

Ici est mon projet de test: une application Windows 8 App Store avec des appels de test pour la version d'origine 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

16
hans

Stephen Taub a mis en œuvre une solution à cette question, voir Création de primitives de coordination async, Partie 7: AsyncReaderWriterLock .

Stephen Taub jouit d'une excellente réputation dans le secteur. Par conséquent, tout ce qu'il écrit est susceptible d'être solide.

Je ne reproduirai pas le code qu'il a publié sur son blog, mais je montrerai 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 utilisez une méthode intégrée au .NET Framework, utilisez plutôt SemaphoreSlim.WaitAsync. Vous n'obtiendrez pas un verrou lecteur/écrivain, mais vous aurez une implémentation éprouvée.

6
Contango

Hmm, semble 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();
        }
    }
}
4
Anton Pogonets

J'ai essayé d'utiliser un moniteur (code ci-dessous) qui semble fonctionner mais qui possède un GOTCHA ... lorsque vous avez plusieurs threads, cela donne ... 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 ne faisais que cela, mais comme c'était dans un contrôleur ASP.NET, il en résultait un blocage.

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

0
andrew pate