Почему я не могу использовать оператор "await" в теле оператора блокировки?

ключевое слово await в C# (.NET Async CTP) не допускается из оператора блокировки.

С MSDN:

An выражение await нельзя использовать в синхронной функции, в запросе выражение в блоке catch или finally обработки исключений заявление,в блоке оператора блокировки, или в небезопасном контексте.

Я предполагаю, что это трудно или невозможно для команда компилятора для реализации по какой-то причине.

Я попытался обойти с помощью оператора 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();
}

однако это работает не так, как ожидалось. Вызов на мониторинг.Выход в пределах ExitDisposable.Dispose, похоже, блокирует бесконечно (большую часть времени), вызывая взаимоблокировки, поскольку другие потоки пытаются получить блокировку. Я подозреваю, что ненадежность моей работы и причина, по которой операторы await не разрешены в Lock statement, каким-то образом связанный.

кто-нибудь знает почему await не допускается в теле оператора блокировки?

8 ответов


Я предполагаю, что это трудно или невозможно для команды компилятора реализовать по какой-то причине.

нет, это не так уж сложно или невозможно реализовать-тот факт, что вы реализовали себя является свидетельством этого факта. Скорее,это невероятно плохая идея и поэтому мы не позволяем этого, чтобы защитить вас от совершения этой ошибки.

вызов монитора.Выход в пределах ExitDisposable.Распоряжаться кажется блок бесконечно (большую часть времени), вызывая взаимоблокировки, поскольку другие потоки пытаются получить блокировку. Я подозреваю, что ненадежность моей работы и причина, по которой операторы await не разрешены в Lock statement, каким-то образом связаны.

правильно, вы обнаружили, почему мы сделали это незаконно. ожидание внутри замка-это рецепт для создания тупиков.

Я уверен, вы можете понять, почему: произвольный код выполняется между возвращениями await управление вызывающему абоненту и метод возобновляется. Этот произвольный код может вынимать замки, которые производят инверсии порядка блокировки, и, следовательно, тупики.

хуже, код может возобновиться в другом потоке (в расширенных сценариях; обычно вы снова поднимаете поток, который ждал, но не обязательно), в этом случае разблокировка будет разблокировкой блокировки в другом потоке, чем поток, который вынул блокировку. Это хорошая идея? Нет.

Я отмечаю, что это также "Худшая практика", чтобы сделать yield return внутри lock, по той же причине. Это законно, но я хотел бы, чтобы мы сделали это незаконным. Мы не собираемся делать ту же ошибку для "wait".


использовать SemaphoreSlim.WaitAsync метод.

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

в основном это было бы неправильно.

есть два способа это мог бы быть реализованы:

  • держите замок, только отпуская его в конце блока.
    Это действительно плохая идея, поскольку вы не знаете, сколько времени займет асинхронная операция. Вы должны только держать замки для минимальный количество времени. Это также потенциально невозможно, как нить владеет блокировка, а не метод - и вы даже не можете выполнить остальную часть асинхронного метода в одном потоке (в зависимости от планировщика задач).

  • отпустите блокировку в await и повторно получите ее, когда await вернется
    Это нарушает принцип наименьшего удивления IMO, где асинхронный метод должен вести себя как можно ближе к эквивалентному синхронному коду-если вы не используете Monitor.Wait в блоке блокировки вы ожидаете владеть блокировка на время блока.

таким образом, в основном здесь есть два конкурирующих требования-вы не должны быть попытка чтобы сделать первый здесь, и если вы хотите принять второй подход, вы можете сделать код намного яснее, имея два отдельных блока блокировки, разделенных выражением await:

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

таким образом, запрещая вам ждать в самом блоке блокировки, язык заставляет вас думать о том, что вы действительно хотите сделать, и сделать этот выбор более ясным в коде, который вы пишете.


это относится к http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/, Windows 8 app store и .net 4.5

вот мой угол зрения на это:

функция языка async/await упрощает многие вещи, но также вводит сценарий, который был редко встречались до того, как было так просто использовать асинхронные вызовы: reentrance.

Это особенно верно для случая обработчики, потому что для многих событий вы понятия не имеете, что происходит после возвращения из обработчика событий. Одна вещь, которая может произойти на самом деле, это то, что асинхронный метод, который вы ждете в первом обработчике событий, вызывается из другого обработчика событий, все еще на та же нить.

вот реальный сценарий, с которым я столкнулся в приложении Windows 8 App store: Мое приложение имеет два фрейма: входя и выходя из фрейма, я хочу загрузить/сохранить некоторые данные в файл/хранилище. Для сохранения и загрузки используются события OnNavigatedTo/From. Сохранение и загрузка выполняется некоторой функцией асинхронной утилиты (например,http://winrtstoragehelper.codeplex.com/). При переходе от кадра 1 к кадру 2 или в другом направлении асинхронная загрузка и безопасные операции вызываются и ожидаются. Обработчики событий становятся асинхронными, возвращая void => их нельзя ждать.

однако первая операция открытия файла (lets говорит: внутри функции сохранения) утилиты асинхронный тоже и поэтому первый await возвращает управление фреймворку, который через некоторое время вызывает другую утилиту (load) через второй обработчик событий. Нагрузка сейчас пытается открыть тот же файл и если файл уже открыт для операции сохранения, сбой с ACCESSDENIED исключением.

минимальным решением для меня является обеспечение доступа к файлу с помощью using и 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);
    }
}

обратите внимание, что его замок по сути блокирует все операции с файлами утилиты только с одним замком, который излишне силен, но отлично работает для моего сценария.

здесь мой тестовый проект: приложение Windows 8 app store с некоторыми тестовыми вызовами для исходной версии из http://winrtstoragehelper.codeplex.com/ и моя модифицированная версия, которая использует AsyncLock от Stephen Toub http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx.

могу ли я также предложить это ссылка на сайт: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx


Это просто расширение до ответ.

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

использование:

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

Хм, выглядит уродливо, кажется, работает.

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

Стивен Тауб реализовал решение этого вопроса, см. Создание Примитивов Асинхронной Координации, Часть 7: AsyncReaderWriterLock.

Стивен Тауб высоко ценится в отрасли, поэтому все, что он пишет, вероятно, будет прочным.

Я не буду воспроизводить код, который он опубликовал в своем блоге, но я покажу, как его использовать:

/// <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.
        }
    }
}

Если вы метод, который запечен в .NET framework, используйте SemaphoreSlim.WaitAsync вместо. Вы не получите блокировка чтения / записи, но вы получите проверенную реализацию.


Я попытался использовать монитор (код ниже), который, кажется, работает, но имеет GOTCHA... когда у вас есть несколько потоков, он даст... Система.Нарезка резьбы.SynchronizationLockException метод синхронизации объекта был вызван из несинхронизированного блока кода.

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;
        }
    }
}

до этого я просто делал это, но это было в ASP.NET контроллер так что это привело к тупику.

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