İş parçacığı açısından güvenli bir Sözlük uygulamanın en iyi yolu nedir?


109

IDictionary türetip özel bir SyncRoot nesnesi tanımlayarak C # 'da iş parçacığı açısından güvenli bir Sözlük uygulayabildim:

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

Daha sonra tüketicilerim boyunca bu SyncRoot nesnesini kilitliyorum (birden çok iş parçacığı):

Misal:

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

Çalışmasını başardım, ancak bu bazı çirkin kodlarla sonuçlandı. Sorum şu, iş parçacığı açısından güvenli bir Sözlüğü uygulamanın daha iyi ve daha zarif bir yolu var mı?


3
Bunun hakkında çirkin bulduğunuz nedir?
Asaf R

1
Sanırım, SharedDictionary sınıfının tüketicileri içindeki kodu boyunca sahip olduğu tüm kilit ifadelerine atıfta bulunuyor - bir SharedDictionary nesnesindeki bir yönteme her eriştiğinde çağıran kodu kilitliyor.
Peter Meyer

Add yöntemini kullanmak yerine, ex-m_MySharedDictionary ["key1"] = "item1" değerlerini atayarak yapmayı deneyin, bu iş parçacığı güvenlidir.
testuser

Yanıtlar:


43

Peter'ın dediği gibi, tüm iş parçacığı güvenliğini sınıfın içinde özetleyebilirsiniz. Açığa çıkardığınız veya eklediğiniz herhangi bir olayda, kilitlerin dışında çağrıldıklarından emin olmanız gerekecek.

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

Düzenleme: MSDN belgeleri, numaralandırmanın doğası gereği iş parçacığı açısından güvenli olmadığını belirtir. Bu, sınıfınızın dışında bir senkronizasyon nesnesini göstermenin bir nedeni olabilir. Buna yaklaşmanın başka bir yolu, tüm üyeler üzerinde bir eylem gerçekleştirmek için bazı yöntemler sağlamak ve üyelerin numaralandırılmasını kilitlemek olabilir. Bununla ilgili sorun, o işleve aktarılan eylemin sözlüğünüzün bir üyesini çağırıp çağırmadığını (bu bir kilitlenmeyle sonuçlanır) bilmemenizdir. Senkronizasyon nesnesini açığa çıkarmak, tüketicinin bu kararları vermesine olanak tanır ve sınıfınızın içindeki kilitlenmeyi gizlemez.


@fryguybob: Senkronizasyon nesnesini ifşa etmemin tek sebebi numaralandırmadır. Geleneksel olarak, bu nesneyi yalnızca koleksiyon boyunca numaralandırırken kilitleyecektim.
GP.

1
Sözlüğünüz çok büyük değilse, bir kopya üzerinde sıralayabilir ve bunu sınıfta yerleşik olarak bulundurabilirsiniz.
fryguybob

2
Sözlüğüm çok büyük değil ve bence bu hile yaptı. Yaptığım şey, özel sözlüğün kopyalarıyla bir Dictionary'nin yeni örneğini döndüren CopyForEnum () adlı yeni bir genel yöntem yapmaktı. Bu yöntem daha sonra numaralandırmalar için çağrıldı ve SyncRoot kaldırıldı. Teşekkürler!
GP.

12
Sözlük işlemleri granüler olma eğiliminde olduğundan, bu aynı zamanda doğası gereği evreli bir sınıf değildir. İf (dict.Contains (ne olursa olsun)) {dict.Remove (her neyse) satırları boyunca küçük bir mantık; dict.Add (ne olursa olsun, yeni değer); } kesinlikle gerçekleşmeyi bekleyen bir yarış durumudur.
kaide

207

Eşzamanlılığı destekleyen .NET 4.0 sınıfı adlandırılır ConcurrentDictionary.


4
Lütfen bunu yanıt olarak işaretleyin, eğer kendi .Net'in bir çözümü varsa özel bir sözlüğe ihtiyacınız yoktur
Alberto León

27
(Diğer cevabın .NET 4.0'ın varlığından çok önce yazıldığını unutmayın (2010'da piyasaya sürüldü).)
Jeppe Stig Nielsen

1
Maalesef kilitsiz bir çözüm değil, bu nedenle SQL Server CLR güvenli derlemelerinde işe yaramaz. Burada anlatılana benzer bir şeye ihtiyacınız olacak: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf veya belki bu uygulama: github.com/hackcraft/Ariadne
Triynko

2
Gerçekten eski, biliyorum, ancak ConcurrentDictionary vs Dictionary kullanmanın önemli performans kayıplarına neden olabileceğine dikkat etmek önemlidir. Bu, büyük olasılıkla pahalı bağlam değiştirmenin sonucudur, bu nedenle kullanmadan önce iş parçacığı açısından güvenli bir sözlüğe ihtiyacınız olduğundan emin olun.
outbred

60

Dahili olarak senkronize etmeye çalışmak neredeyse kesinlikle yetersiz olacaktır çünkü çok düşük bir soyutlama seviyesinde. AddVe ContainsKeyişlemlerini ayrı ayrı aşağıdaki gibi iş parçacığı açısından güvenli hale getirdiğinizi varsayalım:

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

O halde bu sözde iş parçacığı güvenli bit kod parçasını birden çok iş parçacığından çağırdığınızda ne olur? Her zaman iyi çalışır mı?

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

Basit cevap hayır. Bir noktada, Addyöntem, anahtarın sözlükte zaten mevcut olduğunu belirten bir istisna atacaktır. İş parçacığı açısından güvenli bir sözlükle bu nasıl olabilir, sorabilirsiniz? Her işlemin iş parçacığı açısından güvenli olması nedeniyle, iki işlemin birleşimi değildir, çünkü başka bir iş parçacığı çağrınız ContainsKeyveAdd .

Bu, bu tür bir senaryoyu doğru yazmak için sözlüğün dışında bir kilide ihtiyacınız olduğu anlamına gelir , örn.

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

Ama şimdi, harici olarak kilitleme kodu yazmak zorunda olduğunuza göre, dahili ve harici senkronizasyonu karıştırıyorsunuz, bu da her zaman net olmayan kod ve kilitlenme gibi sorunlara yol açıyor. Yani sonuçta muhtemelen şunlardan birini yapmak daha iyidir:

  1. Bir normal kullanın Dictionary<TKey, TValue>ve üzerinde bileşik işlemleri içeren harici olarak senkronize edin veya

  2. Yöntem IDictionary<T>gibi işlemleri birleştiren farklı bir arabirime (yani değil ) sahip yeni bir iş parçacığı güvenli sarmalayıcı yazın, AddIfNotContainedböylece ondan işlemleri birleştirmenize asla gerek kalmaz.

(Ben 1 numara ile gitme eğilimindeyim)


10
NET 4.0'ın, standart koleksiyondan farklı bir arayüze sahip olan koleksiyonlar ve sözlükler gibi bir dizi iş parçacığı güvenli kapsayıcı içereceğini belirtmek gerekir (yani yukarıdaki 2. seçeneği sizin için yapıyorlar).
Greg Beech

1
Ayrıca, temelde yatan sınıfın ne zaman elden çıkarıldığını bilmesini sağlayan uygun bir numaralandırıcı tasarlarsa, kaba kilitlemenin bile sunduğu ayrıntı düzeyinin çoğu zaman tek yazarlı çok okuyuculu bir yaklaşım için yeterli olacağını belirtmek gerekir (sözlüğü yazarken kullanılmamış bir numaralandırıcı var ise sözlüğü bir kopyayla değiştirmelidir).
supercat

6

Özel kilit nesnenizi bir özellik aracılığıyla yayınlamamalısınız. Kilit nesnesi, yalnızca bir buluşma noktası olarak hareket etmek amacıyla özel olarak varolmalıdır.

Standart kilit kullanıldığında performansın zayıf olduğu kanıtlanırsa, Wintellect'in Power Threading kilit koleksiyonu çok yararlı olabilir.


5

Tanımladığınız uygulama yöntemiyle ilgili birkaç sorun var.

  1. Senkronizasyon nesnenizi asla ifşa etmemelisiniz. Bunu yapmak, nesneyi kapıp kilitleyen bir tüketiciye kendinizi açacaktır ve sonra kızardınız.
  2. İş parçacığı güvenli bir sınıfla iş parçacığı olmayan güvenli bir arabirim uyguluyorsunuz. IMHO bu size yolun aşağısına mal olacak

Şahsen, güvenli bir sınıf oluşturmanın en iyi yolunun değişmezlik olduğunu gördüm. İplik güvenliği ile karşılaşabileceğiniz sorunların sayısını gerçekten azaltır. Daha fazla ayrıntı için Eric Lippert'in Bloguna göz atın .


3

Tüketici nesnelerinizde SyncRoot özelliğini kilitlemenize gerek yoktur. Sözlüğün yöntemleri içerisinde sahip olduğunuz kilit yeterlidir.

Detaylandırmak için: Sonunda olan şey, sözlüğünüzün gerekenden daha uzun bir süre boyunca kilitli kalmasıdır.

Sizin durumunuzda olan şudur:

De ki iplik A SyncRoot üzerinde kilit edinme önce m_mySharedDictionary.Add çağrısından aldığını söyleyin. B dişi daha sonra kilidi almaya çalışır ancak bloke edilir. Aslında, diğer tüm konular engellenmiştir. A iş parçacığının Add yöntemini çağırmasına izin verilir. Add yöntemi içindeki lock deyiminde, A iş parçacığının kilidi yeniden almasına izin verilir, çünkü zaten ona sahiptir. Yöntem içinde ve daha sonra yöntemin dışında kilit bağlamından çıkıldığında, iş parçacığı A, diğer iş parçacıklarının devam etmesine izin veren tüm kilitleri serbest bıraktı.

SharedDictionary sınıfınızdaki Add yöntemi aynı etkiye sahip olacağından, herhangi bir tüketicinin Add yöntemini çağırmasına izin verebilirsiniz. Bu noktada, fazladan kilitlemeye sahipsiniz. Sözlük nesnesi üzerinde art arda gerçekleşmesi garanti edilmesi gereken iki işlem gerçekleştirmeniz gerekirse, yalnızca sözlük yöntemlerinden birinin dışında SyncRoot'u kilitlersiniz.


1
Doğru değil ... dahili olarak iş parçacığı güvenli olan birbiri ardına iki işlem yaparsanız, bu genel kod bloğunun iş parçacığı için güvenli olduğu anlamına gelmez. Örneğin: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); }, ContainsKey ve Add iş parçacığı güvenli olsa bile iş parçacığı güvenli olmazdı
Tim

Demek istediğin doğru, ama cevabım ve sorumla bağlam dışında. Soruya bakarsanız, ContainsKey'i aramaktan bahsetmiyor, benim cevabım da yok. Cevabım, orijinal sorudaki örnekte gösterilen SyncRoot'ta bir kilit edinme ile ilgilidir. Kilit ifadesi bağlamında, bir veya daha fazla iş parçacığı güvenli işlem gerçekten güvenli bir şekilde yürütülecektir.
Peter Meyer

Sanırım tek yaptığı sözlüğe eklemekse, ancak "// daha fazla IDictionary üyesi" olduğundan, bir noktada sözlükten verileri geri okumak isteyeceğini varsayıyorum. Durum böyleyse, dışarıdan erişilebilir bir kilitleme mekanizmasının olması gerekir. Sözlüğün kendisindeki SyncRoot veya yalnızca kilitleme için kullanılan başka bir nesne olması önemli değildir, ancak böyle bir şema olmadan, genel kod iş parçacığı açısından güvenli olmayacaktır.
Tim

Harici kilitleme mekanizması, sorudaki örneğinde gösterdiği gibidir: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - aşağıdakileri yapmak tamamen güvenli olacaktır: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} Başka bir deyişle, harici kilitleme mekanizması, SyncRoot genel özelliği üzerinde çalışan kilit ifadesidir.
Peter Meyer

0

Sadece sözlüğü neden yeniden yaratmadığınızı düşündünüz mü? Okuma çok sayıda yazma ise, kilitleme tüm istekleri senkronize edecektir.

misal

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

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.