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

349

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

Из MSDN :

Выражение 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();
}

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

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

Kevin
источник
27
Я полагаю, вы нашли причину, почему это не разрешено.
asawyer
3
Могу ли я предложить эту ссылку: hanselman.com/blog/…, а эту: blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx
Ханс
Я только начинаю догонять и узнавать немного больше об асинхронном программировании. После многочисленных тупиковых ситуаций в моих wpf-приложениях я обнаружил, что эта статья является надежным средством защиты в практике асинхронного программирования. msdn.microsoft.com/en-us/magazine/…
C. Tewalt
Блокировка предназначена для предотвращения асинхронного доступа, когда асинхронный доступ может нарушить ваш код, поэтому, если вы используете асинхронную блокировку внутри блокировки, вы аннулировали блокировку .. поэтому, если вам нужно что-то ждать внутри блокировки, вы не используете блокировку правильно
MikeT

Ответы:

366

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

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

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

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

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

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

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

Эрик Липперт
источник
190
Как вы справляетесь со сценарием, в котором вам нужно вернуть запись в кеш, и если запись не существует, вам нужно асинхронно вычислить содержимое, затем добавить + вернуть запись, убедившись, что никто другой не вызовет вас в это время?
Софтлион
9
Я понимаю, что опоздал на вечеринку здесь, однако я был удивлен, увидев, что вы поставили тупики в качестве основной причины, почему это плохая идея. Я сам пришел к выводу, что возвратный характер блокировки / монитора будет большей частью проблемы. То есть вы помещаете в пул потоков две задачи, которые lock () выполняет в синхронном мире в отдельных потоках. Но теперь с await (если я имею в виду) вы можете выполнить две задачи внутри блока блокировки, потому что поток был повторно использован. Веселье наступает. Или я что-то не так понял?
Гарет Уилсон
4
@GarethWilson: я говорил о тупиках, потому что заданный вопрос касался тупиков . Вы правы, что причудливые проблемы повторного входа возможны и кажутся вероятными.
Эрик Липперт
11
@ Эрик Липперт. Учитывая, что SemaphoreSlim.WaitAsyncкласс был добавлен в .NET Framework задолго до того, как вы опубликовали этот ответ, я думаю, мы можем смело предположить, что это возможно сейчас. Независимо от этого, ваши комментарии о сложности реализации такой конструкции все еще остаются в силе.
Contango
7
«Произвольный код выполняется между моментом, когда await возвращает управление вызывающей стороне, и метод возобновляется», - безусловно, это верно для любого кода, даже в отсутствие async / await, в многопоточном контексте: другие потоки могут выполнять произвольный код в любой время, и произнес произвольный код, как вы говорите, «может быть удаление блокировок, которые производят инверсии порядка блокировки, и, следовательно, тупиков». Так почему это имеет особое значение для async / await? Я понимаю, что второй пункт о том, что «код может возобновиться в другом потоке» имеет особое значение для async / await.
Бакар
291

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

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
user1639030
источник
10
Поскольку этот метод был недавно введен в .NET Framework, я думаю, мы можем предположить, что концепция блокировки в асинхронном / ожидающем мире теперь хорошо доказана.
Contango
5
Для получения дополнительной информации, ищите текст «SemaphoreSlim» в этой статье: Async / Await - Лучшие практики в асинхронном программировании
BobbyA
1
@JamesKo, если все эти задачи ждут результата, Stuffя не вижу в этом никакого пути ...
Охад Шнайдер
7
Разве это не должно быть инициализировано как mySemaphoreSlim = new SemaphoreSlim(1, 1)для того, чтобы работать как lock(...)?
Сергей
3
Добавлена ​​расширенная версия этого ответа: stackoverflow.com/a/50139704/1844247
Сергей
67

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

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

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

  • Освободите блокировку в 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)
{
}

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

Джон Скит
источник
5
Учитывая, что SemaphoreSlim.WaitAsyncкласс был добавлен в .NET Framework задолго до того, как вы опубликовали этот ответ, я думаю, мы можем смело предположить, что это возможно сейчас. Независимо от этого, ваши комментарии о сложности реализации такой конструкции все еще остаются в силе.
Contango
7
@Contango: Ну, это не совсем то же самое. В частности, семафор не привязан к конкретному потоку. Он достигает аналогичных целей для блокировки, но есть существенные различия.
Джон Скит
@JonSkeet я знаю, что это очень старый поток и все такое, но я не уверен, как вызов some () защищен с помощью этих блокировок вторым способом? когда поток выполняет что-то (), любой другой поток может быть вовлечен в это! Я что-то здесь упускаю?
@ Джозеф: Это не защищено в этот момент. Это второй подход, который проясняет, что вы получаете / выпускаете, а затем снова получаете / выпускаете, возможно, в другом потоке. Потому что первый подход - плохая идея, согласно ответу Эрика.
Джон Скит
41

Это просто продолжение этого ответа .

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. 
        });
    }
}
Сергей
источник
1
Может быть опасно получать блокировку семафора за пределами tryблока - если между ними происходит исключение, WaitAsyncи tryсемафор никогда не будет освобожден (тупик). С другой стороны, перемещение WaitAsyncвызова в tryблок создаст другую проблему, когда семафор может быть освобожден без получения блокировки. См. Связанный поток, где эта проблема была объяснена: stackoverflow.com/a/61806749/7889645
AndreyCh
16

Это относится к 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 от Стивена Тауба http: //blogs.msdn. com / b / pfxteam / archive / 2012/02/12 / 10266988.aspx .

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

ганс
источник
7

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

Контанго
источник
Мне любопытно узнать, есть ли какие-либо предостережения относительно использования этого кода. Если кто-то может продемонстрировать какие-либо проблемы с этим кодом, я хотел бы знать. Однако верно то, что концепция асинхронной / ожидающей блокировки определенно хорошо себя зарекомендовала, как SemaphoreSlim.WaitAsyncи в .NET Framework. Все, что делает этот код, это добавляет концепцию блокировки чтения / записи.
Contango
3

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

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();
        }
    }
}
Антон Погонец
источник
0

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

Эндрю Пэйт
источник