Kilit ifadesinin gövdesi içinde neden 'bekliyor' işlecini kullanamıyorum?


348

Bir kilit ifadesinden C # (.NET Async CTP) içindeki anahtar kelimeye izin verilmez.

Kimden MSDN :

Beklenen bir ifade, bir eşzamanlı işlevde, bir sorgu ifadesinde, bir istisna işleme ifadesinin yakalama veya son olarak bloğunda, bir kilit ifadesinin bloğunda veya güvenli olmayan bir bağlamda kullanılamaz.

Bunun, derleyici ekibinin bir nedenden dolayı uygulaması zor ya da imkansız olduğunu düşünüyorum.

Using ifadesi ile bir çalışma girişiminde bulundu:

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

Ancak bu beklendiği gibi çalışmaz. ExitDisposable.Dispose içinde Monitor.Exit çağrısı, diğer iş parçacıkları kilidi almaya çalıştıkça süresiz olarak (çoğu zaman) kilitlenmelere neden oluyor gibi görünüyor. Etrafımdaki çalışmanın güvenilmezliğinden ve kilit ifadesinde ifadeleri beklememe nedeninin bir şekilde ilişkili olduğundan şüpheleniyorum.

Bir kilit ifadesi içinde neden beklemeye izin verilmediğini bilen var mı ?


27
İzin verilmemesinin nedenini bulduğunu hayal edebiliyorum.
asawyer


Zaman uyumsuz programlama hakkında biraz daha bilgi edinmeye ve öğrenmeye başladım. Wpf uygulamalarımdaki sayısız çıkmazdan sonra, bu makalenin zaman uyumsuz programlama uygulamalarında büyük bir güvenlik görevlisi olduğunu gördüm. msdn.microsoft.com/en-us/magazine/…
C. Tewalt

Kilit, eşzamansız erişim kodunuzu kıracağı zaman eşzamansız erişimi önlemek için tasarlanmıştır, bir kilit içinde eşzamansız kullanıyorsanız ergo kilitinizi geçersiz kıldı .. yani kilit içinde bir şey beklemek gerekirse kilidi doğru
kullanmıyorsunuz

Yanıtlar:


366

Bunun, derleyici ekibinin bir nedenden dolayı uygulaması zor ya da imkansız olduğunu düşünüyorum.

Hayır, uygulamak hiç de zor ya da imkansız değil - bunu kendiniz uyguladığınız gerçeği bu gerçeğin bir kanıtıdır. Aksine, bu inanılmaz derecede kötü bir fikir ve bu yüzden sizi bu hatayı yapmaktan korumak için izin vermiyoruz.

ExitDisposable.Dispose içinde Monitor.Exit çağrısı, diğer iş parçacıkları kilidi almaya çalıştıkça süresiz olarak (çoğu zaman) kilitlenmeye neden görünüyor. Etrafımdaki çalışmanın güvenilmezliğinden ve kilit ifadesinde ifadeleri beklememe nedeninin bir şekilde ilişkili olduğundan şüpheleniyorum.

Doğru, neden yasadışı yaptığımızı keşfettiniz. Bir kilidin içinde beklemek, kilitlenme üretmek için bir reçetedir.

Nedenini görebilirsiniz eminim: rasgele kod beklemek arayanın denetim döndürür ve yöntem devam eder arasında çalışır . Bu rasgele kod, kilit düzeni ters çevirmeleri üreten kilitler ve dolayısıyla kilitlenmeler çıkarabilir.

Daha da kötüsü, kod başka bir iş parçacığında devam edebilir (gelişmiş senaryolarda; normalde tekrar bekleyen iş parçacığını tekrar alırsınız, ancak mutlaka değil), bu durumda kilidin açılması, iş parçacığından farklı bir iş parçacığının kilidini açar kilidi dışarı. Bu iyi bir fikir mi? Hayır.

Aynı nedenden ötürü bir yield returniç kısım yapmak da "en kötü uygulama" olduğunu belirtiyorum lock. Bunu yapmak yasal, ama keşke yasa dışı yapmış olsaydık. Aynı hatayı "beklemek" için yapmayacağız.


190
Önbellek girdisini döndürmeniz gereken bir senaryoyu nasıl ele alırsınız ve giriş yoksa, eşzamansız olarak içeriği hesaplamanız ve daha sonra girdiyi eklemeniz ve geri döndürmeniz gerekir.
Softlion

9
Burada partiye geç kaldığımı fark ettim, ancak bunun kötü bir fikir olmasının ana nedeni olarak kilitlenmeleri koyduğunuzu görünce şaşırdım. Kendi fikrimde, kilit / Monitörün yeniden doğanın doğasının sorunun daha büyük bir parçası olacağı sonucuna vardım. Yani, senkronize bir dünyada ayrı iş parçacıklarında yürütülecek olan lock () olan thread havuzuna iki görev kuyruğa alırsınız. Ama şimdi bekliyor (eğer izin veriliyorsa) iş parçacığı yeniden kullanıldığından kilit bloğu içinde iki görev yürütmek olabilir. Hilarity ortaya çıkar. Yoksa bir şeyi yanlış mı anladım?
Gareth Wilson

4
@GarethWilson: Kilitlenmelerden bahsettim çünkü sorulan soru kilitlenmelerden ibaretti . Tuhaf yeniden giriş konularının mümkün ve muhtemel gözüktüğü konusunda haklısınız.
Eric Lippert

11
@Eric Lippert. SemaphoreSlim.WaitAsyncBu yanıtı gönderdikten sonra sınıfın .NET çerçevesine eklendiği göz önüne alındığında, bunun şimdi mümkün olduğunu güvenle varsayabileceğimizi düşünüyorum. Buna bakılmaksızın, böyle bir yapıyı uygulamanın zorluğu hakkındaki yorumlarınız hala tamamen geçerlidir.
Contango

7
"rasgele kod, beklemenin denetimi çağırana döndürdüğü ve yöntem devam ettiği zaman arasında çalışır" - şüphesiz bu, zaman uyumsuz / beklemenin yokluğunda bile, çok iş parçacıklı bir bağlamda herhangi bir kod için geçerlidir: diğer iş parçacıkları herhangi bir anda rasgele kod yürütebilir ve "kilit düzeni ters çevirmeleri üreten kilitler ve dolayısıyla kilitlenmeler olabilir" dedi. Öyleyse bu neden asenkron / beklemede özellikle önemlidir? İkinci nokta yeniden "kod başka bir iş parçacığında devam edebilir" özellikle async / bekliyor için önemli olduğunu anlıyorum.
bacar

291

SemaphoreSlim.WaitAsyncYöntemi kullanın .

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

10
Bu yöntem yakın zamanda .NET çerçevesine dahil edildiğinden, bir async / await dünyasında kilitleme kavramının artık kanıtlanmış olduğunu varsayabiliriz.
Contango

5
Daha fazla bilgi için, bu makalede "SemaphoreSlim" metnini arayın: Async /
Await

1
@JamesKo eğer tüm bu görevler Stuffonun etrafında bir şey göremiyorum sonucunu bekliyorsa ...
Ohad Schneider

7
Gibi mySemaphoreSlim = new SemaphoreSlim(1, 1)çalışacak şekilde başlatılmamalı lock(...)mı?
Sergey

3
Bu yanıtın genişletilmiş sürümünü
Sergey

67

Temelde bu yanlış bir şey olurdu.

Bu iki yolu vardır olabilir uygulanacak:

  • Kilidi tutun, sadece bloğun sonunda serbest bırakın .
    Asenkron işlemin ne kadar süreceğini bilmediğiniz için bu gerçekten kötü bir fikir. Kilitleri yalnızca minimum süre tutmalısınız . Bir iş parçacığı bir yönteme değil, bir kilide sahip olduğundan potansiyel olarak imkansızdır - ve aynı zamanda (iş zamanlayıcısına bağlı olarak) eşzamansız yöntemin geri kalanını yürütemezsiniz.

  • Bekleme sırasında kilidi serbest bırakın ve bekleme döndüğünde yeniden edinin
    Bu, eşzamansız yöntemin eşzamanlı senkron kod gibi olabildiğince yakın davranması gereken en az şaşkınlık IMO ilkesini ihlal eder - Monitor.Waitbir kilit bloğunda kullanmazsanız , bloğun süresi boyunca kilide sahip olmak.

Yani temelde burada iki rakip gereksinim var - ilkini burada yapmaya çalışmamalısınız ve ikinci yaklaşımı almak istiyorsanız, iki ayrı kilit bloğunu bekleyen ifadeyle ayrılmış olarak kodu daha net hale getirebilirsiniz:

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

Dolayısıyla, kilit bloğunun kendisini beklemenizi yasaklayarak, dil sizi gerçekten ne yapmak istediğinizi düşünmeye zorlar ve bu seçimi yazdığınız kodda daha net hale getirir.


5
SemaphoreSlim.WaitAsyncBu yanıtı gönderdikten sonra sınıfın .NET çerçevesine eklendiği göz önüne alındığında, bunun şimdi mümkün olduğunu güvenle varsayabileceğimizi düşünüyorum. Buna bakılmaksızın, böyle bir yapıyı uygulamanın zorluğu hakkındaki yorumlarınız hala tamamen geçerlidir.
Contango

7
@Contango: Eh değil oldukça aynı şey. Özellikle, semafor belirli bir iş parçacığına bağlı değildir. Kilitlemek için benzer hedeflere ulaşır, ancak önemli farklılıklar vardır.
Jon Skeet

@JonSkeet bu çok eski bir iş parçacığı ve tüm biliyorum, ama bir şey () çağrısı nasıl bu kilitleri kullanarak ikinci şekilde korunur emin değilim? bir iş parçacığı bir şey () yürütürken başka bir iş parçacığı da dahil olabilir! Burada bir şey mi eksik?

@Joseph: O noktada korunmuyor. Bu, muhtemelen farklı bir iş parçacığında, edinme / bırakma, sonra tekrar edinme / bırakma işleminizi netleştiren ikinci yaklaşımdır. Çünkü Eric'in cevabına göre ilk yaklaşım kötü bir fikir.
Jon Skeet

41

Bu sadece bu cevabın bir uzantısıdır .

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

Kullanımı:

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
Semafor kilidini tryblokun dışına çıkarmak tehlikeli olabilir - eğer bir istisna oluşursa WaitAsyncve trysemafor asla serbest bırakılmazsa (kilitlenme). Öte yandan, semafor bir kilit alınmadan serbest bırakılabildiğinde, WaitAsyncçağrı trybloğa taşınırsa başka bir sorun ortaya çıkacaktır. Bu sorunun açıklandığı ilgili konuya bakın: stackoverflow.com/a/61806749/7889645
AndreyCh

16

Bu, http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , Windows 8 uygulama mağazası ve .net 4.5 anlamına gelir.

İşte bu açıdan benim açım:

Zaman uyumsuz / beklemede olan dil özelliği birçok şeyi oldukça kolaylaştırır, ancak zaman uyumsuz çağrıları kullanmak çok kolay olmadan önce nadiren karşılaşılan bir senaryo sunar: yeniden giriş.

Bu özellikle olay işleyicileri için geçerlidir, çünkü birçok olay için olay işleyicisinden döndükten sonra neler olduğu hakkında hiçbir fikriniz yoktur. Gerçekte olabilecek bir şey, ilk olay işleyicide beklediğiniz zaman uyumsuz yöntemin, aynı iş parçacığında hala başka bir olay işleyiciden çağrılmasıdır.

İşte bir Windows 8 App store uygulamasında karşılaştığım gerçek bir senaryo: Uygulamamın iki çerçevesi var: bir çerçeveye girip bir çerçeveden ayrılmak Dosya / depolamaya bazı verileri yüklemek / güvence altına almak istiyorum. Kaydetme ve yükleme için OnNavigatedTo / From olayları kullanılır. Kaydetme ve yükleme bazı eşzamansız yardımcı program işlevleri ( http://winrtstoragehelper.codeplex.com/ gibi ) tarafından yapılır . Kare 1'den kare 2'ye veya diğer yöne giderken, eşzamansız yük ve güvenli işlemler çağrılır ve beklenir. Olay işleyicileri zaman uyumsuz hale gelir void => beklenemezler.

Bununla birlikte, yardımcı programın ilk dosya açma işlemi (diyelim: bir kaydetme işlevi içinde) de zaman uyumsuzdur ve bu nedenle ilk, denetimi daha sonra ikinci olay işleyicisi aracılığıyla diğer yardımcı programı (yük) çağıran çerçeveye döndürür. Yük şimdi aynı dosyayı açmaya çalışır ve dosya kayıt işlemi için şimdiye kadar açıksa, ACCESSDENIED istisnasıyla başarısız olur.

Benim için minimum bir çözüm, bir kullanarak ve bir AsyncLock aracılığıyla dosya erişimini güvence altına almaktır.

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

Kilidinin, temelde tek bir kilitle yardımcı programın tüm dosya işlemlerini kilitlediğini unutmayın, bu da gereksiz derecede güçlü ancak senaryom için iyi çalışır.

İşte benim test projem: http://winrtstoragehelper.codeplex.com/ adresinden orijinal sürüm için bazı test çağrıları içeren bir windows 8 app store uygulaması ve Stephen Toub http: //blogs.msdn'den AsyncLock kullanan değiştirilmiş sürümüm. com.tr / b / pfxteam / arşiv / 2012/02/12 / 10266988.aspx .

Bu bağlantıyı da önerebilir miyim: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsenkron EşgüdümPrimitives.aspx


7

Stephen Taub bu soruya bir çözüm uyguladı, bkz. Async Koordinasyon İlkelerini Oluşturma, Bölüm 7: AsyncReaderWriterLock .

Stephen Taub sektörde büyük saygı görüyor, bu yüzden yazdığı her şey muhtemelen sağlam olacak.

Blogunda yayınladığı kodu çoğaltmayacağım, ancak nasıl kullanılacağını göstereceğim:

/// <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 çerçevesine dönüştürülmüş bir yöntem istiyorsanız SemaphoreSlim.WaitAsyncbunun yerine kullanın. Bir okuyucu / yazar kilidi alamazsınız, ancak denenmiş ve test edilmiş bir uygulama alırsınız.


Bu kodu kullanmak için herhangi bir uyarı olup olmadığını bilmek merak ediyorum. Herkes bu kod ile ilgili herhangi bir sorun gösterebilir, bilmek istiyorum. Bununla birlikte, asenkron / beklemede kilitleme kavramının SemaphoreSlim.WaitAsync.NET çerçevesinde olduğu gibi kesinlikle iyi kanıtlanmış olmasıdır. Tüm bu kod bir okuyucu / yazar kilidi konsepti eklemek.
Contango

3

Hmm, çirkin görünüyor, işe yarıyor gibi görünüyor.

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

Çalışıyor gibi görünen ama bir GOTCHA ... sahip bir Monitor (aşağıda kodu) kullanmayı denediniz mi ... birden fazla iş parçacığı varsa ... System.Threading.SynchronizationLockException Nesne eşitleme yöntemi, senkronize olmayan bir kod bloğundan çağrıldı.

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

Bundan önce sadece bunu yapıyordum, ama bir ASP.NET denetleyicisindeydi, bu yüzden bir kilitlenme ile sonuçlandı.

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.