Mengapa saya tidak bisa menggunakan operator 'menunggu' di dalam tubuh pernyataan kunci?

348

Kata kunci yang menunggu dalam C # (.NET Async CTP) tidak diizinkan dari dalam pernyataan kunci.

Dari MSDN :

Ekspresi menunggu tidak dapat digunakan dalam fungsi sinkron, dalam ekspresi query, dalam menangkap atau akhirnya blok pernyataan penanganan pengecualian, di blok pernyataan kunci , atau dalam konteks yang tidak aman.

Saya menganggap ini sulit atau tidak mungkin dilakukan oleh tim penyusun karena beberapa alasan.

Saya mencoba menyelesaikan dengan menggunakan pernyataan:

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

Namun ini tidak berfungsi seperti yang diharapkan. Panggilan ke Monitor.Keluar dalam ExitDisposable.Dispose tampaknya memblokir tanpa batas waktu (sebagian besar waktu) menyebabkan kebuntuan sebagai upaya utas lainnya untuk mendapatkan kunci. Saya menduga tidak dapat diandalkannya pekerjaan saya di sekitar dan alasan menunggu pernyataan tidak diizinkan dalam pernyataan kunci entah bagaimana terkait.

Adakah yang tahu mengapa menunggu tidak diizinkan di dalam tubuh pernyataan kunci?

Kevin
sumber
27
Saya membayangkan Anda menemukan alasan mengapa itu tidak diizinkan.
asawyer
3
Bolehkah saya menyarankan tautan ini: hanselman.com/blog/… dan yang ini: blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx
hans
Saya baru mulai mengejar ketinggalan dan belajar sedikit tentang pemrograman async. Setelah banyak kebuntuan dalam aplikasi WPF saya, saya menemukan artikel ini menjadi penjaga yang aman dalam praktik pemrograman async. msdn.microsoft.com/en-us/magazine/…
C. Tewalt
Kunci dirancang untuk mencegah akses async ketika akses async akan memecahkan kode Anda, jika Anda menggunakan async di dalam kunci, Anda telah membatalkan kunci Anda .. jadi jika Anda perlu menunggu sesuatu di dalam kunci Anda, Anda tidak menggunakan kunci dengan benar
MikeT

Jawaban:

366

Saya menganggap ini sulit atau tidak mungkin dilakukan oleh tim penyusun karena beberapa alasan.

Tidak, sama sekali tidak sulit atau tidak mungkin untuk diterapkan - fakta bahwa Anda menerapkannya sendiri adalah bukti dari fakta itu. Sebaliknya, itu adalah ide yang sangat buruk sehingga kami tidak mengizinkannya, untuk melindungi Anda dari melakukan kesalahan ini.

panggilan ke Monitor. Keluar dalam ExitDisposable.Dispose tampaknya memblokir tanpa batas waktu (sebagian besar waktu) menyebabkan kebuntuan sebagai upaya thread lain untuk mendapatkan kunci. Saya menduga tidak dapat diandalkannya pekerjaan saya di sekitar dan alasan menunggu pernyataan tidak diizinkan dalam pernyataan kunci entah bagaimana terkait.

Benar, Anda telah menemukan mengapa kami membuatnya ilegal. Menunggu di dalam kunci adalah resep untuk menghasilkan kebuntuan.

Saya yakin Anda dapat melihat alasannya: kode arbitrer berjalan antara waktu menunggu pengembalian kontrol ke pemanggil dan metode dilanjutkan . Kode arbitrer itu bisa mengeluarkan kunci yang menghasilkan inversi pemesanan kunci, dan karenanya kebuntuan.

Lebih buruk lagi, kode dapat melanjutkan pada utas lain (dalam skenario lanjutan; biasanya Anda mengambil kembali pada utas yang menunggu, tetapi tidak harus) dalam hal itu membuka kunci akan membuka kunci pada utas yang berbeda dari utas yang mengambil keluar kunci. Apakah itu ide yang bagus? Tidak.

Saya perhatikan bahwa ini juga merupakan "praktik terburuk" untuk melakukan bagian yield returndalam lock, karena alasan yang sama. Itu legal untuk melakukannya, tetapi saya berharap kami membuatnya ilegal. Kami tidak akan membuat kesalahan yang sama untuk "menunggu".

Eric Lippert
sumber
189
Bagaimana Anda menangani skenario di mana Anda perlu mengembalikan entri cache, dan jika entri itu tidak ada, Anda perlu menghitung secara tidak sinkron konten kemudian menambahkan + mengembalikan entri, memastikan tidak ada orang lain yang memanggil Anda sementara itu?
Softlion
9
Saya menyadari bahwa saya terlambat ke pesta di sini, namun saya terkejut melihat Anda memasukkan deadlock sebagai alasan utama mengapa ini adalah ide yang buruk. Saya sampai pada kesimpulan dalam pemikiran saya sendiri bahwa sifat kunci / monitor yang kembali masuk akan menjadi bagian yang lebih besar dari masalah. Artinya, Anda mengantri dua tugas ke kumpulan utas yang mengunci (), yang di dunia sinkron akan dijalankan pada utas terpisah. Tetapi sekarang dengan menunggu (jika diizinkan, maksud saya), Anda dapat memiliki dua tugas yang dijalankan di dalam blok kunci karena utas digunakan kembali. Hilaritas terjadi. Atau apakah saya salah paham akan sesuatu?
Gareth Wilson
4
@ GarethWilson: Saya berbicara tentang deadlock karena pertanyaannya adalah deadlock . Anda benar bahwa masalah pemulangan kembali yang aneh itu mungkin dan tampak mungkin.
Eric Lippert
11
@Eric Lippert. Mengingat bahwa SemaphoreSlim.WaitAsynckelas telah ditambahkan ke .NET framework setelah Anda memposting jawaban ini, saya pikir kita dapat dengan aman berasumsi bahwa itu mungkin sekarang. Terlepas dari ini, komentar Anda tentang sulitnya menerapkan konstruksi semacam itu masih sepenuhnya valid.
Contango
7
"kode arbitrer berjalan antara saat menunggu mengembalikan kontrol ke pemanggil dan metode dilanjutkan" - tentu saja ini berlaku untuk kode apa pun, bahkan tanpa adanya async / menunggu, dalam konteks multithreaded: utas lain dapat mengeksekusi kode arbitrer di setiap waktu, dan mengatakan kode arbitrer seperti yang Anda katakan "bisa mengeluarkan kunci yang menghasilkan inversi kunci pemesanan, dan karenanya kebuntuan." Jadi mengapa ini sangat penting dengan async / menunggu? Saya mengerti poin kedua ulang "kode bisa melanjutkan di utas lain" menjadi signifikansi tertentu untuk async / menunggu.
bacar
291

Gunakan SemaphoreSlim.WaitAsyncmetode.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }
pengguna1639030
sumber
10
Seperti metode ini diperkenalkan ke dalam kerangka NET. Baru-baru ini, saya pikir kita dapat mengasumsikan bahwa konsep mengunci di dunia async / menunggu sekarang terbukti dengan baik.
Contango
5
Untuk beberapa info lebih lanjut, cari teks "SemaphoreSlim" di artikel ini: Async / Await - Praktik Terbaik dalam Pemrograman Asynchronous
BobbyA
1
@JamesKo jika semua tugas itu menunggu hasil dari Stuffsaya tidak melihat jalan keluarnya ...
Ohad Schneider
7
Bukankah seharusnya diinisialisasi seperti mySemaphoreSlim = new SemaphoreSlim(1, 1)untuk bekerja seperti lock(...)?
Sergey
3
Menambahkan versi diperpanjang dari jawaban ini: stackoverflow.com/a/50139704/1844247
Sergey
67

Pada dasarnya itu akan menjadi hal yang salah untuk dilakukan.

Ada dua cara ini bisa diterapkan:

  • Pegang kunci, hanya lepaskan di ujung blok .
    Ini adalah ide yang sangat buruk karena Anda tidak tahu berapa lama operasi asinkron akan berlangsung. Anda hanya boleh memegang kunci untuk waktu yang minimal . Ini juga berpotensi tidak mungkin, karena utas memiliki kunci, bukan metode - dan Anda bahkan tidak dapat menjalankan sisa metode asinkron pada utas yang sama (tergantung pada penjadwal tugas).

  • Lepaskan kunci di tunggu, dan dapatkan kembali ketika menunggu kembali.
    Ini melanggar prinsip IMO yang paling mencengangkan, di mana metode asinkronik harus berperilaku sedekat mungkin seperti kode sinkron yang setara - kecuali jika Anda menggunakan Monitor.Waitdalam blok kunci, Anda berharap untuk memiliki kunci selama durasi blok.

Jadi pada dasarnya ada dua persyaratan yang bersaing di sini - Anda tidak boleh mencoba melakukan yang pertama di sini, dan jika Anda ingin mengambil pendekatan kedua Anda dapat membuat kode lebih jelas dengan memiliki dua blok kunci yang terpisah dipisahkan oleh ekspresi yang menunggu:

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

Jadi dengan melarang Anda menunggu di blok kunci itu sendiri, bahasa memaksa Anda untuk berpikir tentang apa yang benar - benar ingin Anda lakukan, dan membuat pilihan itu lebih jelas dalam kode yang Anda tulis.

Jon Skeet
sumber
5
Mengingat bahwa SemaphoreSlim.WaitAsynckelas telah ditambahkan ke .NET framework setelah Anda memposting jawaban ini, saya pikir kita dapat dengan aman berasumsi bahwa itu mungkin sekarang. Terlepas dari ini, komentar Anda tentang sulitnya menerapkan konstruksi semacam itu masih sepenuhnya valid.
Contango
7
@Contango: Nah itu tidak cukup hal yang sama. Secara khusus, semaphore tidak terikat pada utas tertentu. Itu mencapai tujuan yang sama untuk mengunci, tetapi ada perbedaan yang signifikan.
Jon Skeet
@ JonSkeet saya tahu ini adalah utas lama yang sangat bagus dan semuanya, tapi saya tidak yakin bagaimana panggilan sesuatu () dilindungi menggunakan kunci-kunci itu dengan cara kedua? ketika sebuah thread mengeksekusi sesuatu () utas lainnya dapat terlibat di dalamnya juga! Apakah saya melewatkan sesuatu di sini?
@ Joseph: Ini tidak dilindungi pada saat itu. Ini adalah pendekatan kedua, yang membuatnya jelas bahwa Anda memperoleh / melepaskan, lalu mendapatkan / melepaskan lagi, mungkin pada utas yang berbeda. Karena pendekatan pertama adalah ide yang buruk, sesuai jawaban Eric.
Jon Skeet
41

Ini hanya perpanjangan dari jawaban ini .

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

Pemakaian:

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. 
        });
    }
}
Sergey
sumber
1
Ini bisa berbahaya untuk mendapatkan kunci semaphore di luar tryblok - jika pengecualian terjadi antara WaitAsyncdan trysemaphore tidak akan pernah dirilis (jalan buntu). Di sisi lain, memindahkan WaitAsyncpanggilan ke tryblok akan menimbulkan masalah lain, ketika semaphore dapat dirilis tanpa kunci diperoleh. Lihat utas terkait di mana masalah ini dijelaskan: stackoverflow.com/a/61806749/7889645
AndreyCh
16

Ini merujuk ke http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http://winrtstoragehelper.codeplex.com/ , toko aplikasi Windows 8 dan .net 4.5

Inilah sudut pandang saya tentang ini:

Fitur bahasa async / menunggu membuat banyak hal cukup mudah tetapi juga memperkenalkan skenario yang jarang ditemui sebelum begitu mudah untuk menggunakan panggilan async: reentrance.

Ini terutama berlaku untuk event handler, karena untuk banyak event Anda tidak memiliki petunjuk tentang apa yang terjadi setelah Anda kembali dari event handler. Satu hal yang mungkin benar-benar terjadi adalah, bahwa metode async yang Anda tunggu di event handler pertama, dipanggil dari event handler lain yang masih berada di utas yang sama.

Berikut ini adalah skenario nyata yang saya temui di aplikasi windows 8 App store: Aplikasi saya memiliki dua bingkai: masuk dan keluar dari bingkai saya ingin memuat / menyimpan beberapa data ke file / penyimpanan. Acara OnNavigatedTo / From digunakan untuk menyimpan dan memuat. Penghematan dan pemuatan dilakukan oleh beberapa fungsi utilitas async (seperti http://winrtstoragehelper.codeplex.com/ ). Ketika menavigasi dari frame 1 ke frame 2 atau ke arah lain, beban async dan operasi yang aman dipanggil dan ditunggu. Penangan event menjadi async dengan mengembalikan batal => mereka tidak bisa ditunggu.

Namun, operasi buka file pertama (katakanlah: di dalam fungsi simpan) dari utilitas juga async dan sehingga yang pertama menunggu mengembalikan kontrol ke framework, yang beberapa waktu kemudian memanggil utilitas lain (memuat) melalui event handler kedua. Pemuatan sekarang mencoba untuk membuka file yang sama dan jika file tersebut dibuka sekarang untuk operasi penyimpanan, gagal dengan pengecualian TERAKSESENSI.

Solusi minimum bagi saya adalah mengamankan akses file melalui penggunaan dan 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);
    }
}

Harap dicatat bahwa kuncinya pada dasarnya mengunci semua operasi file untuk utilitas hanya dengan satu kunci, yang tidak perlu kuat tetapi berfungsi dengan baik untuk skenario saya.

Inilah proyek pengujian saya: aplikasi toko aplikasi windows 8 dengan beberapa panggilan uji coba untuk versi asli dari http://winrtstoragehelper.codeplex.com/ dan versi modifikasi saya yang menggunakan AsyncLock dari Stephen Toub http: //blogs.msdn. com / b / pfxteam / arsip / 2012/02/12 / 10266988.aspx .

Bolehkah saya juga menyarankan tautan ini: http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitive.aspx

hans
sumber
7

Stephen Taub telah menerapkan solusi untuk pertanyaan ini, lihat Membangun Async Coordination Primitives, Bagian 7: AsyncReaderWriterLock .

Stephen Taub sangat dihormati di industri ini, jadi apa pun yang ditulisnya mungkin solid.

Saya tidak akan mereproduksi kode yang dia posting di blog-nya, tetapi saya akan menunjukkan kepada Anda bagaimana menggunakannya:

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

Jika Anda menginginkan metode yang dimasukkan ke dalam kerangka .NET, gunakan SemaphoreSlim.WaitAsyncsaja. Anda tidak akan mendapatkan kunci pembaca / penulis, tetapi Anda akan dicoba dan diuji implementasi.

Contango
sumber
Saya ingin tahu apakah ada peringatan untuk menggunakan kode ini. Jika ada yang bisa menunjukkan masalah dengan kode ini, saya ingin tahu. Namun, apa yang benar adalah bahwa konsep penguncian async / menunggu jelas terbukti, seperti SemaphoreSlim.WaitAsyncdalam kerangka NET. Semua kode ini dilakukan adalah menambahkan konsep kunci pembaca / penulis.
Contango
3

Hmm, terlihat jelek, sepertinya berhasil.

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();
        }
    }
}
Anton Pogonets
sumber
0

Saya memang mencoba menggunakan Monitor (kode di bawah) yang tampaknya berfungsi tetapi memiliki GOTCHA ... ketika Anda memiliki beberapa utas akan memberikan ... System.Threading.SynchronizationLockException Metode sinkronisasi objek dipanggil dari blok kode yang tidak disinkronkan.

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

Sebelum ini saya hanya melakukan ini, tapi itu di controller ASP.NET sehingga mengakibatkan kebuntuan.

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

andrew pate
sumber