Чому я не можу використовувати оператора "очікувати" в тілі оператора блокування?


348

Ключове слово, яке очікує в C # (.NET Async CTP), не допускається з-під оператора блокування.

Від MSDN :

Вираз, що очікує, не може використовуватися в синхронній функції, в виразі запиту, у записі або, нарешті, в блоці оператора обробки винятків, в блоці оператора блокування або в небезпечному контексті.

Я припускаю, що це чи то складно, чи неможливо команді-компілятору реалізувати чомусь.

Я спробував обійтись із використанням оператора:

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

Хтось знає, чому очікування не допускається в тілі заяви про блокування?


27
Я б уявив, що ти знайшов причину, чому це не дозволено.
asawyer

3
Я можу запропонувати це посилання: hanselman.com/blog/… та це: blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx
hans

Я тільки починаю наздоганяти і дізнаватися трохи більше про програмування на асинхроні. Після численних тупиків у моїх програмах wpf, я знайшов цю статтю чудовою безпекою у практиці програмування async. msdn.microsoft.com/en-us/magazine/…
C. Tewalt

Блокування призначене для запобігання доступу до асинхронізації, коли доступ до асинхронних систем порушить ваш код, ерго, якщо ви використовуєте async всередині блокування, ви заблокували ваш замок .. тому, якщо вам потрібно чекати чогось всередині вашого блокування, ви не використовуєте блокування правильно
MikeT

Відповіді:


366

Я припускаю, що це чи то складно, чи неможливо команді-компілятору реалізувати чомусь.

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

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

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

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

Гірше, що код може відновитись в іншому потоці (у розширених сценаріях; зазвичай ви знову підбираєте тему, яка зробила очікування, але не обов'язково). з замка. Це гарна ідея? Ні.

Зауважу, що це також "найгірша практика" робити yield returnвнутрішню частину lockз тієї ж причини. Це законно, але я хотів би, щоб ми це зробили незаконним. Ми не збираємось робити ту саму помилку для "чекати".


190
Як ви обробляєте сценарій, коли вам потрібно повернути запис кеша, і якщо запис не існує, вам потрібно обчислити асинхронно вміст, а потім додати + повернути запис, переконуючись, що вас ніхто більше не зателефонує?
Softlion

9
Я усвідомлюю, що тут запізнююся на вечірку, проте мене здивувало, що ти поставив тупики як основну причину, чому це погана ідея. Я прийшов до висновку, вважаючи, що характер блокування / монітора, що повторюється, буде більшою частиною проблеми. Тобто ви ставите в чергу дві задачі до пулу потоків, які фіксують (), що в синхронному світі виконується на окремих потоках. Але тепер з очікуванням (якщо це я маю на увазі) у вас могли бути два завдання, виконані в блоку блокування, оскільки нитка була повторно використана. Настає веселість. Або я щось неправильно зрозумів?
Гарет Вілсон

4
@GarethWilson: Я говорив про тупики, тому що запитання стосувалося тупиків . Ви вірні, що химерні проблеми перенавчання можливі і здаються вірогідними.
Ерік Ліпперт

11
@Eric Lippert. Зважаючи на те, що SemaphoreSlim.WaitAsyncклас був доданий до .NET рамки після того, як ви опублікували цю відповідь, я думаю, ми можемо сміливо припустити, що це можливо і зараз. Незалежно від цього, ваші коментарі щодо складності реалізації такої конструкції все ще цілком справедливі.
Контанго

7
"довільний код працює між часом, коли очікуваний повертає контроль абоненту, і метод поновлюється" - безумовно, це стосується будь-якого коду, навіть за відсутності асинхронізації / очікування, у багатопотоковому контексті: інші потоки можуть виконувати довільний код у будь-якому час і вказав довільний код, як ви говорите, "може бути зняття замків, які створюють інверсії замовлення блокування, а отже, і тупики". То чому це особливе значення має асинхронізація / очікування? Я розумію, що другий пункт про те, що "код може відновитись на іншій нитці", має особливе значення для асинхронізації / очікування.
бакар

291

Використовуйте SemaphoreSlim.WaitAsyncметод.

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

10
Оскільки цей метод був впроваджений у структуру .NET нещодавно, я думаю, що ми можемо припустити, що концепція блокування у світі асинхрон / очікування зараз добре зарекомендувала себе.
Контанго

5
Для отримання додаткової інформації знайдіть текст "SemaphoreSlim" у цій статті: Async / Await - Найкращі практики в асинхронному програмуванні
BobbyA

1
@JamesKo, якщо всі ці завдання чекають результату, Stuffя не бачу цього способу ...
Охад Шнайдер

7
Чи не слід його ініціалізувати так mySemaphoreSlim = new SemaphoreSlim(1, 1), щоб працювати як lock(...)?
Сергій

3
Додана розширена версія цієї відповіді: stackoverflow.com/a/50139704/1844247
Сергій

67

В основному це було б неправильно робити.

Є два способи цього може бути реалізовано:

  • Тримайте замок, лише відпустивши його в кінці блоку .
    Це дійсно погана ідея, оскільки ви не знаєте, скільки часу триватиме асинхронна операція. Ви повинні утримувати замки лише протягом мінімальної кількості часу. Це також потенційно неможливо, як потоці належить замок, а не метод - і ви навіть не можете виконати решту асинхронного методу в одному потоці (залежно від планувальника завдань).

  • Відпустіть замок у режимі очікування та повторно придбайте його, коли чекає повернення.
    Це порушує принцип найменшого здивування IMO, коли асинхронний метод повинен поводитись максимально тісно, ​​як еквівалентний синхронний код - якщо ви не використовуєтеMonitor.Wait блокування блокування, ви розраховуєте, що володіти замком протягом тривалості блоку.

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

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

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


5
Зважаючи на те, що SemaphoreSlim.WaitAsyncклас був доданий до .NET рамки після того, як ви опублікували цю відповідь, я думаю, ми можемо сміливо припустити, що це можливо і зараз. Незалежно від цього, ваші коментарі щодо складності реалізації такої конструкції все ще цілком справедливі.
Контанго

7
@Contango: Ну це не зовсім те саме. Зокрема, семафор не прив'язаний до конкретної нитки. Він досягає подібних цілей, щоб заблокувати, але є значні відмінності.
Джон Скіт

@JonSkeet Я знаю, що це стара давня нитка, і все, але я не впевнений, як захищено дзвінок something (), використовуючи ці блоки вдруге? коли нитка виконує щось (), будь-який інший потік може також долучитися до цього! Я чогось тут пропускаю?

@Joseph: На цей момент це не захищено. Це другий підхід, який дає зрозуміти, що ви купуєте / випускаєте, а потім купуєте / випускаєте знову, можливо, з іншої теми. Тому що перший підхід - це погана ідея, відповідно до відповіді Еріка.
Джон Скіт

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

Ось мій кут на це:

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

Особливо це стосується обробників подій, оскільки для багатьох подій у вас немає поняття про те, що відбувається після повернення з обробника подій. Одне, що може насправді трапитися, це те, що метод асинхронізації, якого ви очікуєте в обробці першого події, викликається від іншого обробника подій, який все ще знаходиться в тій самій ниті.

Ось справжній сценарій, який я натрапив на додаток у магазині Windows 8 App: У моїй програмі є два кадри: входження та вихід із кадру, я хочу завантажити / зберегти деякі дані для файлу / зберігання. Для збереження та завантаження використовуються події OnNavigatedTo / From. Збереження та завантаження виконується деякою функцією утиліти async (наприклад, 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/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitive.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-рамку, скористайтеся SemaphoreSlim.WaitAsyncнатомість. Ви не отримаєте блокування читача / письменника, але ви отримаєте перевірену реалізацію.


Мені цікаво знати, чи є якісь застереження щодо використання цього коду. Якщо хтось може продемонструвати якісь проблеми з цим кодом, я хотів би знати. Однак правда полягає в тому, що поняття блокування асинхрон / очікування, безумовно, добре зарекомендувало себе, як SemaphoreSlim.WaitAsyncі в рамках .NET. Все, що цей код робить - це додати концепцію блокування читача / письменника.
Контанго

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.