Dlaczego nie mogę użyć operatora 'wait' w treści instrukcji lock?

Słowo kluczowe wait w C# (. Net Async CTP) nie jest dozwolone z poziomu instrukcji lock.

From MSDN:

Na wyrażenie oczekujące nie może być użyte w funkcji synchronicznej, w zapytaniu wyrażenie, w catch lub wreszcie bloku obsługi wyjątków Instrukcja, w bloku instrukcji lock , lub w niebezpiecznym kontekście.

Zakładam, że jest to albo trudne, albo niemożliwe dla zespołu kompilatorów wdrożyć z jakiegoś powodu.

Spróbowałem obejść z użyciem instrukcji 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();
}

Jednak nie działa to zgodnie z oczekiwaniami. Wezwanie do monitorowania.Wyjście wewnątrz ExitDisposable.Dispose wydaje się blokować w nieskończoność (przez większość czasu), powodując blokady, gdy inne wątki próbują zdobyć blokadę. Podejrzewam, że nierzetelność mojej pracy i powód oczekiwania na oświadczenia nie są dozwolone w instrukcji blokady są w jakiś sposób powiązane.

Czy ktoś wie dlaczego czeka nie jest dozwolone w treści oświadczenia o blokadzie?

Author: Kevin, 2011-09-30

8 answers

Zakładam, że jest to albo trudne, albo niemożliwe do zaimplementowania przez zespół kompilatorów z jakiegoś powodu.

Nie, Nie Jest to wcale trudne lub niemożliwe do wdrożenia-fakt, że wdrożyłeś go samodzielnie, jest świadectwem tego faktu. Raczej, jest to niewiarygodnie zły pomysł i dlatego nie pozwalamy na to, aby chronić Cię przed popełnieniem tego błędu.
Telefon do monitora.Wyjście wewnątrz ExitDisposable.Dispose wydaje się blokować w nieskończoność (większość czas), powodując blokady, gdy inne wątki próbują zdobyć blokadę. Podejrzewam, że nierzetelność mojej pracy i powód oczekiwania na oświadczenia nie są dozwolone w instrukcji blokady są w jakiś sposób powiązane.
/ Align = "left" / oczekiwanie wewnątrz zamka jest receptą na wytwarzanie martwych punktów.

Jestem pewien, że widzisz dlaczego: arbitralny kod działa między czasem, w którym kontrola wait zwraca do wywołującego i metoda wznawia. Ten arbitralny kod może usuwać blokady, które wywołują inwersje kolejności blokad, a tym samym impasy.

Co gorsza, kod może zostać wznowiony w innym wątku (w zaawansowanych scenariuszach; normalnie można ponownie odebrać w wątku, który oczekiwał, ale niekoniecznie) w takim przypadku odblokowanie byłoby odblokowaniem blokady w innym wątku niż wątek, który wyjął blokadę. Czy to dobry pomysł? Nie.

Zauważam, że jest to również "najgorsza praktyka", aby zrobić yield return wewnątrz a lock, z tego samego powodu. Jest to legalne, ale żałuję, że nie uczyniliśmy tego nielegalnym. Nie popełnimy tego samego błędu przy "oczekiwaniu".

 398
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-30 15:36:58

Użycie SemaphoreSlim.WaitAsync metoda.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
 320
Author: user1639030,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-10-16 09:23:53

W Zasadzie to byłoby złe.

Istnieją dwa sposoby implementacji tego : ]}

  • Trzymać zamek, zwalniając go tylko na końcu bloku.
    Jest to naprawdę zły pomysł, ponieważ nie wiesz, jak długo potrwa operacja asynchroniczna. Zamki należy trzymać tylko przez minimalną ilość czasu. Jest to również potencjalnie niemożliwe, ponieważ wątek posiada blokadę, a nie metodę - i możesz nawet nie wykonaj resztę metody asynchronicznej w tym samym wątku(w zależności od harmonogramu zadań).

  • Zwolnij blokadę w poczekalni i odzyskaj ją po powrocie oczekiwania
    Narusza to zasadę najmniejszego zdziwienia IMO, gdzie metoda asynchroniczna powinna zachowywać się jak najbliżej, jak to możliwe, jak równoważny kod synchroniczny - chyba że użyjesz Monitor.Wait w bloku blokady, spodziewasz się, że blok będzie posiadał blokadę na czas trwania bloku.

Więc Zasadniczo istnieją dwa konkurujące ze sobą wymagania - nie powinieneś próbować zrobić pierwszego tutaj, a jeśli chcesz skorzystać z drugiego podejścia, możesz uczynić kod znacznie jaśniejszym, mając dwa oddzielne bloki blokujące oddzielone wyrażeniem oczekującym:

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

Więc zabraniając Ci czekania w samym bloku blokady, język zmusza cię do myślenia o tym, co naprawdę chcesz zrobić, i czyniąc ten wybór jaśniejszym w kodzie, który piszesz.

 70
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-30 15:29:48

To tylko rozszerzenie do tej odpowiedzi .

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

    // overloading variant for non-void methods with return type (generic T)
    public async Task<T> LockAsync<T>(Func<Task<T>> worker)
    {
        await _semaphore.WaitAsync();

        T result = default;

        try
        {
            result = await worker();
        }
        finally
        {
            _semaphore.Release();
        }

        return result;
    }
}

Użycie:

public class Test
{
    private static readonly SemaphoreLocker _locker = new SemaphoreLocker();

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [async] calls can be used within this block 
            // to handle a resource by one thread. 
        });
        // OR
        var result = await _locker.LockAsync(async () =>
        {
            // [async] calls can be used within this block 
            // to handle a resource by one thread. 
        }
    }
}
 51
Author: Sergey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-30 16:40:40

To odnosi się do http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/, Windows 8 app store i. Net 4.5

Oto mój kąt na to:

Funkcja języka asynchronicznego/oczekującego ułatwia wiele rzeczy, ale wprowadza również scenariusz, który był rzadko spotykane, zanim było tak łatwe w użyciu wywołania asynchroniczne: reentrance.

Jest to szczególnie prawdziwe w przypadku obsługi zdarzeń, ponieważ w przypadku wielu wydarzeń można nie masz pojęcia, co się dzieje po powrocie z obsługi wydarzenia. Jedną z rzeczy, która może się wydarzyć, jest to, że metoda asynchroniczna, którą oczekujesz w pierwszym programie obsługi zdarzeń, zostanie wywołana z innego programu obsługi zdarzeń, wciąż na ten sam wątek.

Oto prawdziwy scenariusz, na który natknąłem się w aplikacji windows 8 App store: Moja aplikacja ma dwie klatki: wchodząc i wychodząc z ramki chcę załadować / zabezpieczyć niektóre dane do pliku / przechowywania. Zdarzenia OnNavigatedTo/From służą do zapisywania i Ładuję. Zapisywanie i ładowanie odbywa się za pomocą funkcji asynchronicznej (np. http://winrtstoragehelper.codeplex.com / ). Podczas przechodzenia od klatki 1 do klatki 2 lub w przeciwnym kierunku wywoływane są i oczekiwane operacje asynchroniczne i bezpieczne. Procedury obsługi zdarzeń stają się asynchroniczne, zwracając void => nie można ich oczekiwać.

Jednak pierwsza operacja otwierania pliku (lets mówi: inside a save function) narzędzia jest również asynchroniczna i tak pierwszy czeka zwraca kontrolę do framework, który jakiś czas później wywołuje inne narzędzie (load) za pośrednictwem drugiej obsługi zdarzenia. Load próbuje teraz otworzyć ten sam plik i jeśli plik jest już otwarty dla operacji save, nie powiedzie się z wyjątkiem accessdenied.

Minimalnym rozwiązaniem dla mnie jest zabezpieczenie dostępu do pliku za pomocą i 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);
    }
}

Proszę zauważyć, że jego blokada zasadniczo blokuje wszystkie operacje plików dla narzędzia za pomocą tylko jednej blokady, która jest niepotrzebnie silna, ale działa dobrze dla mojego scenariusz.

Tutaj jest mój projekt testowy: aplikacja windows 8 app store z niektórymi wywołaniami testowymi dla oryginalnej wersji z http://winrtstoragehelper.codeplex.com / i moja zmodyfikowana wersja, która używa AsyncLock od Stephena Touba http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx .

Mogę też zasugerować ten link: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx

 18
Author: hans,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-03 17:25:00

Stephen Taub wdrożył rozwiązanie tego problemu, zobacz Building Async Coordination Primitives, Część 7: AsyncReaderWriterLock .

Stephen Taub jest wysoko ceniony w branży, więc wszystko, co napisze, może być solidne.

Nie będę powielał kodu, który zamieścił na swoim blogu, ale pokażę Ci, jak go używać:

/// <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.
        }
    }
}

Jeśli chcesz użyć metody, która jest zapisana w. NET framework, użyj SemaphoreSlim.WaitAsync. Nie dostaniesz blokady czytnika/pisarza, ale zostanie wypróbowana i przetestowana implementacja.

 8
Author: Contango,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-08 08:08:40

Hmm, wygląda brzydko, wydaje się działać.

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();
        }
    }
}
 2
Author: Anton Pogonets,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-04 22:40:02

Próbowałem użyć Monitora (kod poniżej), który wydaje się działać, ale ma GOTCHA... gdy masz wiele wątków to da... System.Gwintowanie.SynchronizationLockException metoda synchronizacji obiektu została wywołana z niezsynchronizowanego bloku kodu.

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

Wcześniej po prostu to robiłem, ale to było w ASP.NET kontroler, więc spowodowało to impas.

public async Task<FooResponse> ModifyFooAsync() { lock(lockObject) { return SomeFunctionToModifyFooAsync.Result; } }

 -1
Author: andrew pate,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-01 11:01:02