Почему я не могу использовать оператор 'await' в теле оператора блокировки? - PullRequest
296 голосов
/ 30 сентября 2011

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

С MSDN :

выражение await нельзя использовать в синхронной функции в запросе выражение, в блоке 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, кажется, блокируется на неопределенное время (большую часть времени), вызывая взаимоблокировки, когда другие потоки пытаются получить блокировку. Я подозреваю, что ненадежность моей работы и заявления о причине ожидания не допускаются в операторе блокировки, как-то связаны.

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

Ответы [ 8 ]

322 голосов
/ 30 сентября 2011

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

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

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

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

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

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

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

241 голосов
/ 15 августа 2013

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

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
65 голосов
/ 30 сентября 2011

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

Существует два способа, которыми может быть реализовано:

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

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

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

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

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

20 голосов
/ 02 мая 2018

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

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. 
        });
    }
}
16 голосов
/ 26 декабря 2012

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

Вот мой взгляд на это:

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

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

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

Стивен Тауб реализовал решение этого вопроса, см. Создание асинхронных координационных примитивов, часть 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 вместо.У вас не будет блокировки чтения / записи, но вы получите проверенную и проверенную реализацию.

4 голосов
/ 05 июня 2013

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

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();
        }
    }
}
1 голос
/ 01 сентября 2017

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...