it-swarm.com.ru

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

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

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

294
Kevin

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

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

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

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

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

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

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

321
Eric Lippert

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

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

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

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

Это относится к 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

15
hans

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

6
Contango

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

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
Anton Pogonets

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

1
andrew pate