Apa cara terbaik untuk mengimplementasikan Dictionary yang aman untuk thread?


109

Saya dapat menerapkan Kamus aman-utas di C # dengan mengambil dari IDictionary dan mendefinisikan objek SyncRoot pribadi:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

Saya kemudian mengunci objek SyncRoot ini di seluruh konsumen saya (banyak utas):

Contoh:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

Saya bisa membuatnya berhasil, tetapi ini menghasilkan beberapa kode yang jelek. Pertanyaan saya adalah, apakah ada cara yang lebih baik dan lebih elegan untuk mengimplementasikan Dictionary thread-safe?


3
Apa yang menurut Anda jelek tentang itu?
Asaf R

1
Saya pikir dia mengacu pada semua pernyataan kunci yang dia miliki di seluruh kodenya dalam konsumen kelas SharedDictionary - dia mengunci kode panggilan setiap kali dia mengakses metode pada objek SharedDictionary.
Peter Meyer

Daripada menggunakan metode Tambah, coba lakukan dengan menetapkan nilai ex- m_MySharedDictionary ["key1"] = "item1", ini thread safe.
testuser

Jawaban:


43

Seperti yang dikatakan Peter, Anda dapat merangkum semua keamanan thread di dalam kelas. Anda harus berhati-hati dengan peristiwa apa pun yang Anda ungkapkan atau tambahkan, pastikan bahwa peristiwa tersebut dipanggil di luar kunci apa pun.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

Edit: Dokumen MSDN menunjukkan bahwa enumerasi secara inheren tidak aman untuk thread. Itu bisa menjadi salah satu alasan untuk mengekspos objek sinkronisasi di luar kelas Anda. Cara lain untuk mendekatinya adalah dengan menyediakan beberapa metode untuk melakukan tindakan pada semua anggota dan mengunci sekitar pencacahan anggota. Masalahnya adalah Anda tidak tahu apakah tindakan yang diteruskan ke fungsi itu memanggil beberapa anggota kamus Anda (yang akan mengakibatkan kebuntuan). Mengekspos objek sinkronisasi memungkinkan konsumen membuat keputusan tersebut dan tidak menyembunyikan kebuntuan di dalam kelas Anda.


@fryguybob: Penghitungan sebenarnya satu-satunya alasan mengapa saya mengekspos objek sinkronisasi. Secara konvensi, saya akan melakukan penguncian pada objek itu hanya ketika saya menghitung melalui koleksi.
GP.

1
Jika kamus Anda tidak terlalu besar, Anda dapat menghitung pada salinan dan memilikinya di dalam kelas.
fryguybob

2
Kamus saya tidak terlalu besar, dan saya pikir itu berhasil. Apa yang saya lakukan adalah membuat metode publik baru yang disebut CopyForEnum () yang mengembalikan instance baru dari Dictionary dengan salinan kamus pribadi. Metode ini kemudian dipanggil untuk enumarations, dan SyncRoot telah dihapus. Terima kasih!
GP.

12
Ini juga bukan kelas threadsafe yang melekat karena operasi kamus cenderung bersifat terperinci. Sedikit logika di sepanjang baris if (dict.Contains (terserah)) {dict.Remove (terserah); dict.Add (terserah, newval); } adalah kondisi balapan yang menunggu untuk terjadi.
alas

207

Kelas .NET 4.0 yang mendukung konkurensi dinamai ConcurrentDictionary.


4
Harap tandai ini sebagai tanggapan, Anda tidak memerlukan kamus khusus jika milik .Net memiliki solusi
Alberto León

27
(Ingatlah bahwa jawaban lain ditulis jauh sebelum keberadaan .NET 4.0 (dirilis pada 2010).)
Jeppe Stig Nielsen

1
Sayangnya ini bukan solusi bebas kunci, jadi tidak berguna dalam rakitan aman SQL Server CLR. Anda memerlukan sesuatu seperti yang dijelaskan di sini: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf atau mungkin implementasi ini: github.com/hackcraft/Ariadne
Triynko

2
Benar-benar tua, saya tahu, tetapi penting untuk dicatat bahwa menggunakan ConcurrentDictionary vs a Dictionary dapat mengakibatkan kerugian kinerja yang signifikan. Ini kemungkinan besar hasil dari pengalihan konteks yang mahal, jadi pastikan Anda memerlukan kamus yang aman untuk thread sebelum menggunakannya.
mengalahkan

60

Mencoba menyinkronkan secara internal hampir pasti tidak cukup karena ini pada tingkat abstraksi yang terlalu rendah. Katakanlah Anda membuat operasi Adddan satu ContainsKeyper satu thread-safe sebagai berikut:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

Lalu apa yang terjadi ketika Anda menyebut bit kode yang seharusnya aman untuk thread ini dari beberapa thread? Apakah akan selalu berhasil?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

Jawaban sederhananya adalah tidak. Pada titik tertentu, Addmetode ini akan menampilkan pengecualian yang menunjukkan bahwa kunci tersebut sudah ada di kamus. Bagaimana ini bisa terjadi dengan kamus aman utas, Anda mungkin bertanya? Hanya karena setiap operasi aman untuk thread, kombinasi dua operasi tidak, karena thread lain dapat memodifikasinya antara panggilan Anda ke ContainsKeydanAdd .

Yang berarti untuk menulis skenario jenis ini dengan benar, Anda memerlukan kunci di luar kamus, mis

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

Tapi sekarang, mengingat Anda harus menulis kode penguncian eksternal, Anda mencampur sinkronisasi internal dan eksternal, yang selalu menyebabkan masalah seperti kode yang tidak jelas dan kebuntuan. Jadi pada akhirnya Anda mungkin lebih baik:

  1. Gunakan normal Dictionary<TKey, TValue>dan sinkronkan secara eksternal, menyertakan operasi gabungan di atasnya, atau

  2. Tulis pembungkus thread-safe baru dengan antarmuka berbeda (yaitu tidak IDictionary<T>) yang menggabungkan operasi seperti AddIfNotContainedmetode sehingga Anda tidak perlu menggabungkan operasi darinya.

(Saya cenderung memilih # 1 sendiri)


10
Perlu diperhatikan bahwa .NET 4.0 akan menyertakan sejumlah besar wadah thread-safe seperti koleksi dan kamus, yang memiliki antarmuka berbeda dengan koleksi standar (yaitu mereka melakukan opsi 2 di atas untuk Anda).
Greg Beech

1
Perlu juga dicatat bahwa perincian yang ditawarkan bahkan oleh penguncian kasar akan sering cukup untuk pendekatan multi-pembaca penulis tunggal jika seseorang merancang enumerator yang sesuai yang memungkinkan kelas yang mendasari tahu kapan itu dibuang (metode yang ingin menulis kamus sementara ada enumerator yang tidak diinginkan harus mengganti kamus dengan salinan).
supercat

6

Anda tidak boleh mempublikasikan objek kunci pribadi Anda melalui properti. Objek kunci harus ada secara pribadi dengan tujuan bertindak sebagai titik pertemuan.

Jika kinerja terbukti buruk dengan menggunakan kunci standar, maka koleksi kunci Wintellect Power Threading dapat sangat berguna.


5

Ada beberapa masalah dengan metode implementasi yang Anda gambarkan.

  1. Anda tidak boleh mengekspos objek sinkronisasi Anda. Melakukan hal itu akan membuka diri Anda terhadap konsumen yang mengambil benda itu dan menguncinya, lalu Anda bersulang.
  2. Anda menerapkan antarmuka aman non-utas dengan kelas aman utas. IMHO ini akan merugikan Anda di jalan

Secara pribadi, saya telah menemukan cara terbaik untuk menerapkan kelas aman utas adalah melalui kekekalan. Ini benar-benar mengurangi jumlah masalah yang dapat Anda hadapi dengan keamanan utas. Lihat Blog Eric Lippert untuk lebih jelasnya.


3

Anda tidak perlu mengunci properti SyncRoot di objek konsumen Anda. Kunci yang Anda miliki dalam metode kamus sudah cukup.

Untuk Menguraikan: Apa yang akhirnya terjadi adalah kamus Anda dikunci untuk jangka waktu yang lebih lama dari yang diperlukan.

Yang terjadi dalam kasus Anda adalah sebagai berikut:

Katakanlah utas A memperoleh kunci pada SyncRoot sebelumnya panggilan ke m_mySharedDictionary.Add. Thread B kemudian mencoba mendapatkan kunci tetapi diblokir. Faktanya, semua utas lainnya diblokir. Thread A diizinkan untuk memanggil ke metode Add. Pada pernyataan kunci dalam metode Tambah, utas A diizinkan untuk mendapatkan kunci lagi karena sudah memiliki itu. Setelah keluar dari konteks kunci di dalam metode dan kemudian di luar metode, utas A telah melepaskan semua kunci yang memungkinkan utas lain untuk melanjutkan.

Anda cukup mengizinkan konsumen mana pun untuk memanggil ke metode Tambah karena pernyataan kunci dalam metode Add kelas SharedDictionary Anda akan memiliki efek yang sama. Pada saat ini, Anda memiliki penguncian yang berlebihan. Anda hanya akan mengunci SyncRoot di luar salah satu metode kamus jika Anda harus melakukan dua operasi pada objek kamus yang perlu dijamin untuk terjadi secara berurutan.


1
Tidak benar ... jika Anda melakukan dua operasi demi operasi yang secara internal aman untuk thread, itu tidak berarti bahwa blok kode secara keseluruhan adalah thread aman. Misalnya: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } tidak akan threadsafe, meskipun ContainsKey dan Add adalah threadsafe
Tim

Maksud Anda benar, tetapi di luar konteks dengan jawaban dan pertanyaan saya. Jika Anda melihat pertanyaannya, itu tidak berbicara tentang memanggil ContainsKey, juga tidak menjawab saya. Jawaban saya mengacu pada memperoleh kunci di SyncRoot yang ditunjukkan pada contoh di pertanyaan awal. Dalam konteks pernyataan kunci, satu atau beberapa operasi thread-safe memang akan dijalankan dengan aman.
Peter Meyer

Saya kira jika yang dia lakukan hanyalah menambahkan ke kamus, tetapi karena dia memiliki "// lebih banyak anggota IDictionary ...", saya berasumsi pada titik tertentu dia juga akan ingin membaca kembali data dari kamus. Jika demikian, maka perlu ada mekanisme penguncian yang dapat diakses secara eksternal. Tidak masalah apakah itu SyncRoot dalam kamus itu sendiri atau objek lain yang hanya digunakan untuk mengunci, tetapi tanpa skema seperti itu, keseluruhan kode tidak akan aman untuk thread.
Tim

Mekanisme penguncian eksternal adalah seperti yang dia tunjukkan dalam contoh di pertanyaan: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - akan sangat aman untuk melakukan hal berikut: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} Dengan kata lain, mekanisme penguncian eksternal adalah pernyataan kunci yang beroperasi pada SyncRoot milik publik.
Peter Meyer

0

Hanya berpikir mengapa tidak membuat ulang kamus? Jika membaca banyak tulisan maka penguncian akan menyinkronkan semua permintaan.

contoh

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.