实现线程安全字典的最佳方式是什么?

时间:2022-10-22 10:09:49

I was able to implement a thread-safe Dictionary in C# by deriving from IDictionary and defining a private SyncRoot object:

通过从IDictionary导出并定义一个私有SyncRoot对象,我能够在c#中实现线程安全的字典:

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

I then lock on this SyncRoot object throughout my consumers (multiple threads):

然后我将这个SyncRoot对象锁定到我的整个消费者(多个线程):

Example:

例子:

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

I was able to make it work, but this resulted in some ugly code. My question is, is there a better, more elegant way of implementing a thread-safe Dictionary?

我可以让它工作,但这导致了一些难看的代码。我的问题是,有没有更好、更优雅的方法来实现线程安全的字典?

8 个解决方案

#1


43  

As Peter said, you can encapsulate all of the thread safety inside the class. You will need to be careful with any events you expose or add, making sure that they get invoked outside of any locks.

正如Peter所说,您可以封装类中的所有线程安全性。您将需要谨慎处理您公开或添加的任何事件,确保它们在任何锁之外调用。

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: The MSDN docs point out that enumerating is inherently not thread safe. That can be one reason for exposing a synchronization object outside your class. Another way to approach that would be to provide some methods for performing an action on all members and lock around the enumerating of the members. The problem with this is that you don't know if the action passed to that function calls some member of your dictionary (that would result in a deadlock). Exposing the synchronization object allows the consumer to make those decisions and doesn't hide the deadlock inside your class.

编辑:MSDN文档指出,枚举本质上不是线程安全的。这可能是在类之外公开同步对象的一个原因。另一种方法是提供一些方法,用于对所有成员执行操作并锁定成员的枚举。这样做的问题是,您不知道传递给该函数的操作是否调用了字典中的某个成员(这会导致死锁)。公开同步对象允许使用者做出这些决定,并且不会隐藏您的类中的死锁。

#2


200  

The .NET 4.0 class that supports concurrency is named ConcurrentDictionary.

支持并发的。net 4.0类被命名为ConcurrentDictionary。

#3


58  

Attempting to synchronize internally will almost certainly be insufficient because it's at too low a level of abstraction. Say you make the Add and ContainsKey operations individually thread-safe as follows:

尝试在内部同步几乎肯定是不够的,因为它的抽象级别太低。假设您将Add和ContainsKey操作单独线程安全如下:

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

Then what happens when you call this supposedly thread-safe bit of code from multiple threads? Will it always work OK?

那么当您从多个线程调用这个假定为线程安全的代码时,会发生什么呢?它会一直正常工作吗?

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

The simple answer is no. At some point the Add method will throw an exception indicating that the key already exists in the dictionary. How can this be with a thread-safe dictionary, you might ask? Well just because each operation is thread-safe, the combination of two operations is not, as another thread could modify it between your call to ContainsKey and Add.

答案很简单:不。在某些时候,Add方法将抛出一个异常,表明该键已经存在于字典中。你可能会问,对于线程安全的字典,这是怎么回事?因为每个操作都是线程安全的,所以两个操作的组合不是,因为另一个线程可以在调用ContainsKey和Add时修改它。

Which means to write this type of scenario correctly you need a lock outside the dictionary, e.g.

这意味着要正确地编写这种类型的场景,您需要在字典之外设置一个锁。

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

But now, seeing as you're having to write externally locking code, you're mixing up internal and external synchronisation, which always leads to problems such as unclear code and deadlocks. So ultimately you're probably better to either:

但是现在,由于您必须编写外部锁定代码,所以您混合了内部和外部同步,这总是导致诸如不清晰的代码和死锁之类的问题。所以最终你可能会更好:

  1. Use a normal Dictionary<TKey, TValue> and synchronize externally, enclosing the compound operations on it, or

    使用普通字典 并在外部同步,将复合操作封装在其中,或 ,>

  2. Write a new thread-safe wrapper with a different interface (i.e. not IDictionary<T>) that combines the operations such as an AddIfNotContained method so you never need to combine operations from it.

    编写一个新的线程安全包装器,该包装器具有不同的接口(即不包含IDictionary ),该接口结合了操作,比如添加了addifnotcontains方法,因此您永远不需要从它组合操作。

(I tend to go with #1 myself)

(我自己倾向于选择#1)

#4


6  

You shouldn't publish your private lock object through a property. The lock object should exist privately for the sole purpose of acting as a rendezvous point.

您不应该通过属性发布您的私有锁对象。锁对象应该单独存在,以作为一个会合点。

If performance proves to be poor using the standard lock then Wintellect's Power Threading collection of locks can be very useful.

如果事实证明使用标准锁的性能很差,那么Wintellect的力量线程收集的锁可能非常有用。

#5


4  

There are several problems with implementation method you are describing.

您正在描述的实现方法有几个问题。

  1. You shouldn't ever expose your synchronization object. Doing so will open up yourself to a consumer grabbing the object and taking a lock on it and then you're toast.
  2. 您不应该公开同步对象。这样做会让你自己打开大门,让消费者抓住物品,锁上它,然后你就被烤焦了。
  3. You're implementing a non-thread safe interface with a thread safe class. IMHO this will cost you down the road
  4. 您正在实现一个线程安全类的非线程安全接口。哦,这将使你付出代价

Personally, I've found the best way to implement a thread safe class is via immutability. It really reduces the number of problems you can run into with thread safety. Check out Eric Lippert's Blog for more details.

就我个人而言,我发现实现线程安全类的最佳方法是通过不可变性。它确实减少了您可能遇到的线程安全问题的数量。看看Eric Lippert的博客了解更多细节。

#6


2  

You don't need to lock the SyncRoot property in your consumer objects. The lock you have within the methods of the dictionary is sufficient.

您不需要锁定使用者对象中的SyncRoot属性。字典方法中的锁是足够的。

To Elaborate: What ends up happening is that your dictionary is locked for a longer period of time than is necessary.

需要说明的是:你的字典被锁的时间比需要的时间长。

What happens in your case is the following:

你的情况是这样的:

Say thread A acquires the lock on SyncRoot before the call to m_mySharedDictionary.Add. Thread B then attempts to acquire the lock but is blocked. In fact, all other threads are blocked. Thread A is allowed to call into the Add method. At the lock statement within the Add method, thread A is allowed to obtain the lock again because it already owns it. Upon exiting the lock context within the method and then outside the method, thread A has released all locks allowing other threads to continue.

假设线程A在调用m_mySharedDictionary.Add之前获取SyncRoot上的锁。然后,线程B试图获取锁,但被阻塞。事实上,所有其他线程都被阻塞。线程A可以调用Add方法。在Add方法中的lock语句中,线程A允许再次获取锁,因为它已经拥有锁。在方法中退出锁上下文,然后在方法之外退出锁上下文之后,线程A释放了所有允许其他线程继续的锁。

You can simply allow any consumer to call into the Add method as the lock statement within your SharedDictionary class Add method will have the same effect. At this point in time, you have redundant locking. You would only lock on SyncRoot outside of one of the dictionary methods if you had to perform two operations on the dictionary object that needed to be guaranteed to occur consecutively.

您可以允许任何使用者调用Add方法,因为SharedDictionary类Add方法中的锁语句具有相同的效果。此时,就有了冗余锁定。如果您必须对dictionary对象执行两个操作,而这些操作必须保证连续发生,那么您将只锁定一个dictionary方法之外的SyncRoot。

#7


0  

Just a thought why not recreate the dictionary? If reading is a multitude of writing then locking will synchronize all requests.

为什么不重新创建字典呢?如果阅读是大量的书写,那么锁将同步所有的请求。

example

例子

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

#8


-5  

Collections And Synchronization

集合和同步

#1


43  

As Peter said, you can encapsulate all of the thread safety inside the class. You will need to be careful with any events you expose or add, making sure that they get invoked outside of any locks.

正如Peter所说,您可以封装类中的所有线程安全性。您将需要谨慎处理您公开或添加的任何事件,确保它们在任何锁之外调用。

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: The MSDN docs point out that enumerating is inherently not thread safe. That can be one reason for exposing a synchronization object outside your class. Another way to approach that would be to provide some methods for performing an action on all members and lock around the enumerating of the members. The problem with this is that you don't know if the action passed to that function calls some member of your dictionary (that would result in a deadlock). Exposing the synchronization object allows the consumer to make those decisions and doesn't hide the deadlock inside your class.

编辑:MSDN文档指出,枚举本质上不是线程安全的。这可能是在类之外公开同步对象的一个原因。另一种方法是提供一些方法,用于对所有成员执行操作并锁定成员的枚举。这样做的问题是,您不知道传递给该函数的操作是否调用了字典中的某个成员(这会导致死锁)。公开同步对象允许使用者做出这些决定,并且不会隐藏您的类中的死锁。

#2


200  

The .NET 4.0 class that supports concurrency is named ConcurrentDictionary.

支持并发的。net 4.0类被命名为ConcurrentDictionary。

#3


58  

Attempting to synchronize internally will almost certainly be insufficient because it's at too low a level of abstraction. Say you make the Add and ContainsKey operations individually thread-safe as follows:

尝试在内部同步几乎肯定是不够的,因为它的抽象级别太低。假设您将Add和ContainsKey操作单独线程安全如下:

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

Then what happens when you call this supposedly thread-safe bit of code from multiple threads? Will it always work OK?

那么当您从多个线程调用这个假定为线程安全的代码时,会发生什么呢?它会一直正常工作吗?

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

The simple answer is no. At some point the Add method will throw an exception indicating that the key already exists in the dictionary. How can this be with a thread-safe dictionary, you might ask? Well just because each operation is thread-safe, the combination of two operations is not, as another thread could modify it between your call to ContainsKey and Add.

答案很简单:不。在某些时候,Add方法将抛出一个异常,表明该键已经存在于字典中。你可能会问,对于线程安全的字典,这是怎么回事?因为每个操作都是线程安全的,所以两个操作的组合不是,因为另一个线程可以在调用ContainsKey和Add时修改它。

Which means to write this type of scenario correctly you need a lock outside the dictionary, e.g.

这意味着要正确地编写这种类型的场景,您需要在字典之外设置一个锁。

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

But now, seeing as you're having to write externally locking code, you're mixing up internal and external synchronisation, which always leads to problems such as unclear code and deadlocks. So ultimately you're probably better to either:

但是现在,由于您必须编写外部锁定代码,所以您混合了内部和外部同步,这总是导致诸如不清晰的代码和死锁之类的问题。所以最终你可能会更好:

  1. Use a normal Dictionary<TKey, TValue> and synchronize externally, enclosing the compound operations on it, or

    使用普通字典 并在外部同步,将复合操作封装在其中,或 ,>

  2. Write a new thread-safe wrapper with a different interface (i.e. not IDictionary<T>) that combines the operations such as an AddIfNotContained method so you never need to combine operations from it.

    编写一个新的线程安全包装器,该包装器具有不同的接口(即不包含IDictionary ),该接口结合了操作,比如添加了addifnotcontains方法,因此您永远不需要从它组合操作。

(I tend to go with #1 myself)

(我自己倾向于选择#1)

#4


6  

You shouldn't publish your private lock object through a property. The lock object should exist privately for the sole purpose of acting as a rendezvous point.

您不应该通过属性发布您的私有锁对象。锁对象应该单独存在,以作为一个会合点。

If performance proves to be poor using the standard lock then Wintellect's Power Threading collection of locks can be very useful.

如果事实证明使用标准锁的性能很差,那么Wintellect的力量线程收集的锁可能非常有用。

#5


4  

There are several problems with implementation method you are describing.

您正在描述的实现方法有几个问题。

  1. You shouldn't ever expose your synchronization object. Doing so will open up yourself to a consumer grabbing the object and taking a lock on it and then you're toast.
  2. 您不应该公开同步对象。这样做会让你自己打开大门,让消费者抓住物品,锁上它,然后你就被烤焦了。
  3. You're implementing a non-thread safe interface with a thread safe class. IMHO this will cost you down the road
  4. 您正在实现一个线程安全类的非线程安全接口。哦,这将使你付出代价

Personally, I've found the best way to implement a thread safe class is via immutability. It really reduces the number of problems you can run into with thread safety. Check out Eric Lippert's Blog for more details.

就我个人而言,我发现实现线程安全类的最佳方法是通过不可变性。它确实减少了您可能遇到的线程安全问题的数量。看看Eric Lippert的博客了解更多细节。

#6


2  

You don't need to lock the SyncRoot property in your consumer objects. The lock you have within the methods of the dictionary is sufficient.

您不需要锁定使用者对象中的SyncRoot属性。字典方法中的锁是足够的。

To Elaborate: What ends up happening is that your dictionary is locked for a longer period of time than is necessary.

需要说明的是:你的字典被锁的时间比需要的时间长。

What happens in your case is the following:

你的情况是这样的:

Say thread A acquires the lock on SyncRoot before the call to m_mySharedDictionary.Add. Thread B then attempts to acquire the lock but is blocked. In fact, all other threads are blocked. Thread A is allowed to call into the Add method. At the lock statement within the Add method, thread A is allowed to obtain the lock again because it already owns it. Upon exiting the lock context within the method and then outside the method, thread A has released all locks allowing other threads to continue.

假设线程A在调用m_mySharedDictionary.Add之前获取SyncRoot上的锁。然后,线程B试图获取锁,但被阻塞。事实上,所有其他线程都被阻塞。线程A可以调用Add方法。在Add方法中的lock语句中,线程A允许再次获取锁,因为它已经拥有锁。在方法中退出锁上下文,然后在方法之外退出锁上下文之后,线程A释放了所有允许其他线程继续的锁。

You can simply allow any consumer to call into the Add method as the lock statement within your SharedDictionary class Add method will have the same effect. At this point in time, you have redundant locking. You would only lock on SyncRoot outside of one of the dictionary methods if you had to perform two operations on the dictionary object that needed to be guaranteed to occur consecutively.

您可以允许任何使用者调用Add方法,因为SharedDictionary类Add方法中的锁语句具有相同的效果。此时,就有了冗余锁定。如果您必须对dictionary对象执行两个操作,而这些操作必须保证连续发生,那么您将只锁定一个dictionary方法之外的SyncRoot。

#7


0  

Just a thought why not recreate the dictionary? If reading is a multitude of writing then locking will synchronize all requests.

为什么不重新创建字典呢?如果阅读是大量的书写,那么锁将同步所有的请求。

example

例子

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

#8


-5  

Collections And Synchronization

集合和同步