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

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

От MSDN:

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

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

Я попытался работать с использованием инструкции:

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

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

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

Ответ 1

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

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

вызвать Monitor.Exit внутри ExitDisposable. Кажется, что блокирование бесконечно (большую часть времени) вызывает логические блокировки, так как другие потоки пытаются получить блокировку. Я подозреваю, что ненадежность моей работы и аргументы ожидания ожидания не допускаются в инструкции блокировки, как-то связаны.

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

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

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

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

Ответ 2

Используйте метод SemaphoreSlim.WaitAsync.

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

Ответ 3

В принципе, это было бы неправильно.

Это можно реализовать двумя способами:

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

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

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

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

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

Ответ 4

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

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

Ответ 5

Это относится к http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx, http://winrtstoragehelper.codeplex.com/, Хранилище приложений Windows 8 и .net 4.5

Вот мой угол:

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

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

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

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

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

Ответ 6

Стивен Тауб реализовал решение этого вопроса, см. Создание асинхронных координационных примитивов, часть 7: AsyncReaderWriterLock.

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

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

/// <summary>
///     Demo class for reader/writer lock that supports async/await.
///     For source, see Stephen Taub 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. Вы не получите блокировку чтения/записи, но вы получите испытанную и проверенную реализацию.

Ответ 7

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

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

Ответ 8

Я попытался использовать монитор (код ниже), который работает, но имеет GOTCHA... когда у вас есть несколько потоков, которые он даст... System.Threading.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; } }