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

Le mot-clé await en C# (. NET async CTP) n'est pas autorisé à partir d'une instruction lock.

À Partir de MSDN:

Un l'expression await ne peut pas être utilisée dans une fonction synchrone, dans une requête expression, dans le catch ou enfin le bloc d'une gestion d'exception dans le bloc d'une instruction de verrouillage , ou dans un contexte non sécurisé.

Je suppose que c'est difficile ou impossible pour l'équipe du compilateur à implémenter pour certains raison.

J'ai essayé un travail 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 à surveiller.Quitter dans ExitDisposable.Jetez semble bloquer indéfiniment (la plupart du temps) provoquant des blocages que 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 instructions await ne sont pas autorisées dans l'instruction lock sont en quelque sorte liées.

Quelqu'un sait - pourquoi attendent n'est pas autorisé dans le corps d'une déclaration de verrouillage?

260
demandé sur Kevin 2011-09-30 19:23:00

8 réponses

Je suppose que c'est difficile ou impossible pour l'équipe du compilateur à implémenter pour une raison quelconque.

Non, ce n'est pas du tout difficile ou impossible à mettre en œuvre-le fait que vous l'avez mis en œuvre vous-même en témoigne. Au contraire, c'est une idée incroyablement mauvaise et donc nous ne le permettons pas, afin de vous protéger de faire cette erreur.

Appel pour surveiller.Quitter dans ExitDisposable.Jetez semble bloquer indéfiniment (la plupart des temps) provoquant des blocages que 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 instructions await ne sont pas autorisées dans l'instruction lock sont en quelque sorte liées.

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

Je suis sûr que vous pouvez voir pourquoi: le code arbitraire s'exécute entre le moment où l'attente renvoie le contrôle à l'appelant et la méthode reprend . Ce code arbitraire pourrait prendre des verrous qui produisent des inversions de commande de verrou, 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 fait l'attente, mais pas nécessairement) auquel cas le déverrouillage déverrouillerait un verrou sur un thread différent du thread qui a sorti le verrou. Est-ce une bonne idée? Aucun.

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

292
répondu Eric Lippert 2011-09-30 15:36:58

Utiliser SemaphoreSlim.WaitAsync la méthode.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
203
répondu user1639030 2013-08-15 16:54:08

Fondamentalement, ce serait la mauvaise chose à faire.

Il y a deux façons dont ce pourrait être implémenté:

  • Maintenez la serrure, ne la relâchant qu'à la fin du bloc .
    C'est une très mauvaise idée que vous ne savez pas combien de temps l'opération asynchrone va prendre. Vous ne devez tenir les verrous que pendant Un minimum de temps. C'est aussi potentiellement impossible, en tant que thread possède un verrou, pas une méthode - et vous ne pouvez même pas exécutez le reste de la méthode asynchrone sur le même thread (en fonction du Planificateur de tâches).

  • Libérer le verrou dans l'attendent, et acquérir il lorsque l'attendent retourne
    Cela viole le principe de moindre étonnement IMO, où la méthode asynchrone doit se comporter aussi étroitement que possible comme le code synchrone équivalent-sauf si vous utilisez 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 concurrentes ici - vous ne devriez pas essayer de faire la première ici, et si vous voulez prendre 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 await:

// 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, la langue vous oblige à penser à ce que vous voulez vraiment Faire, et à rendre ce choix plus clair dans le code que vous écrivez.

60
répondu Jon Skeet 2011-09-30 15:29:48

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 angle à ce sujet:

La fonction de langage async / await rend beaucoup de choses assez faciles, mais elle introduit également un scénario qui était rarement rencontré avant qu'il soit si facile d'utiliser des appels asynchrones: réentrance.

Ceci est particulièrement vrai pour les gestionnaires d'événements, car pour de nombreux événements vous 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 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 réel que je suis tombé sur une application Windows 8 App store: Mon application a deux cadres: en entrant et en sortant d'un cadre, je veux charger/sécuriser des données dans un fichier/stockage. Les événements OnNavigatedTo/From sont utilisés pour la sauvegarde et chargement. L'enregistrement et le chargement sont effectués par une fonction utilitaire asynchrone (comme http://winrtstoragehelper.codeplex.com/). Lorsque vous naviguez de l'image 1 à l'image 2 ou dans l'autre sens, la charge asynchrone et les opérations de sécurité sont appelées et attendues. Les gestionnaires d'événements deviennent asynchrones retournant 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 aussi asynchrone et ainsi la première attente renvoie le contrôle à la framework, qui appelle plus tard l'autre utilitaire (load) via le deuxième gestionnaire d'événements. La charge essaie maintenant d'ouvrir le même fichier et si le fichier est ouvert maintenant 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 une utilisation 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 de fichiers pour l'utilitaire avec un seul verrou, qui est inutilement fort mais fonctionne bien pour mon scénario.

Voici mon projet de test: une application Windows 8 App store avec des appels de test pour la version originale de http://winrtstoragehelper.codeplex.com/{[3] } et ma version modifiée qui utilise L'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

15
répondu hans 2015-06-03 17:25:00

C'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. 
        });
    }
}
12
répondu Sergey 2018-05-02 16:47:09

Hmm, semble laid, 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
répondu Anton Pogonets 2013-06-04 22:40:02

Stephen Taub a implémenté une solution à cette question, voir Construire des Primitives de Coordination asynchrones, 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 vais pas reproduire 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 une méthode qui est cuit dans le. Net framework, utilisez SemaphoreSlim.WaitAsync à la place. Vous n'obtiendrez pas de verrou de lecteur/écrivain, mais vous obtiendrez essayé et testé de mise en œuvre.

4
répondu Contango 2014-09-12 08:39:04

J'ai essayé d'utiliser un moniteur (code ci-dessous) qui semble fonctionner mais a un GOTCHA... lorsque vous avez plusieurs threads, il donnera... Système.Le filetage.La méthode de synchronisation d'objet SynchronizationLockException 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 ASP.NET contrôleur de sorte qu'il a abouti à une impasse.

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

1
répondu andrew pate 2017-09-01 11:01:02