【问题标题】:Why is ConcurrentDictionary.AddOrUpdate method slow?为什么 ConcurrentDictionary.AddOrUpdate 方法很慢?
【发布时间】:2013-04-24 02:04:11
【问题描述】:

我正在开发一个线程安全的多值字典。在内部,该字典使用了一个并发字典(.net 4.0),其中一个自定义链接列表作为值。在链接列表中添加了相同的关键项。问题是当我使用并发字典的 AddOrUpdate 方法 (approach 1) 插入项目时,与使用 TryGetValue 方法检查键是否存在相比,代码运行速度有点慢然后在锁内手动添加或更新值(方法 2)。使用第一种方法插入 300 万条记录大约需要 20 秒,而使用第二种方法在同一台机器上大约需要 9.5 秒(Intel i3 第二代 2.2 ghz & 4 Gb ram)。一定有什么我还没弄明白的东西丢失了。

我还检查了并发字典的代码,但它似乎和我在锁内做的事情一样:

public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
    {
        if (key == null) throw new ArgumentNullException("key"); 
        if (addValueFactory == null) throw new ArgumentNullException("addValueFactory");
        if (updateValueFactory == null) throw new ArgumentNullException("updateValueFactory"); 

        TValue newValue, resultingValue;
        while (true) 
        {
            TValue oldValue;
            if (TryGetValue(key, out oldValue))
            //key exists, try to update 
            {
                newValue = updateValueFactory(key, oldValue); 
                if (TryUpdate(key, newValue, oldValue)) 
                {
                    return newValue; 
                }
            }
            else //try add
            { 
                newValue = addValueFactory(key);
                if (TryAddInternal(key, newValue, false, true, out resultingValue)) 
                { 
                    return resultingValue;
                } 
            }
        }
    }

这是线程安全多值字典的代码(方法 2 已注释,取消注释以检查差异)。

更新:下面还有删除、添加等方法我没有贴出来。

class ValueWrapper<U, V>
{
    private U _key;
    private V _value;

    public ValueWrapper(U key, V value)
    {
        this._key = key;
        this._value = value;
    }

    public U Key
    {
        get { return _key; }
    }

    public V Value
    {
        get { return _value; }
        set { _value = value; }
    }
}

class LinkNode<Type>
{
    public LinkNode(Type data)
    {
        Data = data;
    }
    public LinkNode<Type> Next;
    public Type Data;
}

public class SimpleLinkedList<T> 
{
    #region Instance Member Variables
    private LinkNode<T> _startNode = null;
    private LinkNode<T> _endNode = null;
    private int _count = 0;

    #endregion

    public void AddAtLast(T item)
    {
        if (_endNode == null)
            _endNode = _startNode = new LinkNode<T>(item);
        else
        {
            LinkNode<T> node = new LinkNode<T>(item);
            _endNode.Next = node;
            _endNode = node;
        }

        _count++;
    }

    public T First
    {
        get { return _startNode == null ? default(T) : _startNode.Data; }
    }

    public int Count
    {
        get { return _count; }
    }

}

class MultiValThreadSafeDictionary<U, T>
{
    private ConcurrentDictionary<U, SimpleLinkedList<ValueWrapper<U, T>>> _internalDictionary;

    private ReaderWriterLockSlim _slimLock = new ReaderWriterLockSlim();


    public MultiValThreadSafeDictionary()
    {
        _internalDictionary = new ConcurrentDictionary<U, SimpleLinkedList<ValueWrapper<U, T>>>(2, 100);
    }

    public T this[U key]
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            /* ****Approach 1 using AddOrUpdate**** */


            _internalDictionary.AddOrUpdate(key, (x) =>
            {
                SimpleLinkedList<ValueWrapper<U, T>> list = new SimpleLinkedList<ValueWrapper<U, T>>();
                ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
                list.AddAtLast(vw);
                //_internalDictionary[key] = list;

                return list;
            },

            (k, existingList) =>
            {
                try
                {
                    _slimLock.EnterWriteLock();

                    if (existingList.Count == 0)
                    {
                        ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
                        existingList.AddAtLast(vw);
                    }
                    else
                        existingList.First.Value = value;

                    return existingList;
                }
                finally
                {
                    _slimLock.ExitWriteLock();
                }
            });


            /* ****Approach 2 not using AddOrUpdate**** */

            /*
            try
            {
                _slimLock.EnterWriteLock();

                SimpleLinkedList<ValueWrapper<U, T>> list;
                if (!_internalDictionary.TryGetValue(key, out list))
                {
                    list = new SimpleLinkedList<ValueWrapper<U, T>>();
                    ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);

                    list.AddAtLast(vw);

                    _internalDictionary[key] = list;
                    //_iterator.AddAtLast(vw);
                    return;
                }

                if (list.Count == 0)
                {
                    ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
                    list.AddAtLast(vw);
                    //_iterator.AddAtLast(vw);
                }
                else
                    list.First.Value = value;
            }
            finally
            {
                _slimLock.ExitWriteLock();
            }
            */

        }
    }
}

测试代码只插入项目,所有项目都有唯一的键。如下。

MultiValThreadSafeDictionary<string, int> testData = new MultiValThreadSafeDictionary<string, int>();

    Task t1 = new Task(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                testData[i.ToString()] = i;
            }
        }
    );

    Task t2 = new Task(() =>
    {
        for (int i = 1000000; i < 2000000; i++)
        {
            testData[i.ToString()] = i;
        }
    }
    );

    Task t3 = new Task(() =>
    {
        for (int i = 2000000; i < 3000000; i++)
        {
            testData[i.ToString()] = i;
        }
    }
    );

    Stopwatch watch = new Stopwatch();
    watch.Start();

    t1.Start();
    t2.Start();
    t3.Start();

    t1.Wait();
    t2.Wait();
    t3.Wait();

    watch.Stop();

    Console.WriteLine("time taken:" + watch.ElapsedMilliseconds);

更新 1:

根据“280Z28”的回答,我重新表述了这个问题。为什么 GetOrAdd 和“我的”方法花费几乎相同的时间,而在我的方法中,我需要一个额外的锁并且还调用 TryAndGet 方法。以及与 AddOrGet 相比,为什么 AddOrUpdate 需要双倍的时间。所有方法的代码如下:

ConcurrentDictionary (.net 4) 中的GetOrAdd 和AddOrUpdate 方法代码如下:

public TValue GetOrAdd(TKey key, TValue value)
{
    if (key == null) throw new ArgumentNullException("key");
    TValue resultingValue;
    TryAddInternal(key, value, false, true, out resultingValue); 
    return resultingValue; 
}

public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
{
    if (key == null) throw new ArgumentNullException("key"); 
    if (addValueFactory == null) throw new ArgumentNullException("addValueFactory");
    if (updateValueFactory == null) throw new ArgumentNullException("updateValueFactory"); 

    TValue newValue, resultingValue;
    while (true) 
    {
        TValue oldValue;
        if (TryGetValue(key, out oldValue))
        //key exists, try to update 
        {
            newValue = updateValueFactory(key, oldValue); 
            if (TryUpdate(key, newValue, oldValue)) 
            {
                return newValue; 
            }
        }
        else //try add
        { 
            newValue = addValueFactory(key);
            if (TryAddInternal(key, newValue, false, true, out resultingValue)) 
            { 
                return resultingValue;
            } 
        }
    }
}

GetOrAdd 在我的代码中使用如下(耗时 9 秒):

SimpleLinkedList<ValueWrapper<U, T>> existingList = new SimpleLinkedList<ValueWrapper<U, T>>();
existingList = _internalDictionary.GetOrAdd(key, existingList);
try
{
    _slimLock.EnterWriteLock();

    if (existingList.Count == 0)
    {
        ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
        existingList.AddAtLast(vw);
    }
    else
        existingList.First.Value = value;
}
finally
{
    _slimLock.ExitWriteLock();
}

AddOrUpdate 使用如下(所有添加需要 20 秒,没有更新)。如其中一个答案所述,这种方法不适合更新。

_internalDictionary.AddOrUpdate(key, (x) =>
{
    SimpleLinkedList<ValueWrapper<U, T>> list = new SimpleLinkedList<ValueWrapper<U, T>>();
    ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
    list.AddAtLast(vw);
    return list;
},

(k, existingList ) =>
{
    try
    {
        _slimLock.EnterWriteLock();

        if (existingList.Count == 0)
        {
            ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
            existingList.AddAtLast(vw);
        }
        else
            existingList.First.Value = value;

        return existingList;
    }
    finally
    {
        _slimLock.ExitWriteLock();
    }
});

没有 AddOrGet 和 AddOrUpdate 的代码如下(耗时 9.5 秒):

try
{
    _slimLock.EnterWriteLock();

    VerySimpleLinkedList<ValueWrapper<U, T>> list;
    if (!_internalDictionary.TryGetValue(key, out list))
    {
        list = new VerySimpleLinkedList<ValueWrapper<U, T>>();
        ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);

        list.AddAtLast(vw);

        _internalDictionary[key] = list;
        return;
    }

    if (list.Count == 0)
    {
        ValueWrapper<U, T> vw = new ValueWrapper<U, T>(key, value);
        list.AddAtLast(vw);
    }
    else
        list.First.Value = value;
}
finally
{
    _slimLock.ExitWriteLock();
}

【问题讨论】:

  • 顺便说一句,你怎么能有一个 i3 core 2 duo.. 你可以有一个 Intel Core i3 第二代或第三代。或者您可以拥有 Intel Core 2 Duo
  • 是的,这是一个混淆。它的英特尔 i3 第二代。感谢您指出这一点。
  • 考虑到您的工作负载是 100% 非冲突写入,“始终独占锁定”方法更快,这很奇怪。这是方法 2 的最坏情况,也是方法 1 的最佳情况。在方法 1 中,只有“add”委托被调用,对吧?永远不会调用更新委托。
  • ConcurrentDictionary 针对常见情况进行了优化,这种情况很少发生争用,并且需要通过在很短的时间内持有锁来提高并发性。但是,您正在测试不常见的情况,即极端争用。除了添加更多任务之外,您无法让它变得更糟。因此,看到对基本操作只进行一次锁定比在存在争用时锁定多次更快也就不足为奇了。可靠地分析这一点并不简单,您必须测试真实的生产代码。
  • @HansPassant 他的写入 100% 是在非冲突键上。他应该看到的唯一争用是虚假的锁分条争用。

标签: c# multithreading concurrentdictionary


【解决方案1】:

您不应为此代码使用AddOrUpdate。这一点非常清楚,因为您的更新方法实际上从未更新存储在ConcurrentDictionary 中的值——它总是返回不变的existingList 参数。相反,您应该执行以下操作。

SimpleLinkedList<ValueWrapper<U, T>> list = _internalDictionary.GetOrAdd(key, CreateEmptyList);
// operate on list here

...

private static SimpleLinkedList<ValueWrapper<U, T>> CreateEmptyList()
{
    return new SimpleLinkedList<ValueWrapper<U, T>>();
}

【讨论】:

  • 好点。我已将其替换为 GetOrAdd,运行时间已提高到 9.5 秒(AddorUpdate 为 20 秒)。但我没有得到的是“其他”代码(使用 TryGetValue 在锁内添加或更新)也需要相同的时间,即 9.5 秒,但它确实“更多”工作。此外,我的测试代码只插入唯一键,“更新”在 AddOrUpdate 中不起作用。
【解决方案2】:

对字典的读操作是以无锁的方式执行的。 如http://msdn.microsoft.com/en-us/library/dd287191.aspx中所述

AddOrUpdate 的实现是使用细粒度锁来检查项目是否已经存在,但是当您第一次自己阅读时,无锁阅读会更快,并且这样做可以减少现有项目所需的锁。

【讨论】:

  • 但是在读取一个项目之前,使用 TryGetValue 方法,我正在获取一个写锁。这不会导致相同的开销吗?
  • 您的锁与并发字典的锁无关。当然你的锁很慢,但是 Add0rUpdate 方法有它自己的内部锁。使用并发字典时不需要任何类型的锁定,锁定是自动管理的。
  • 我肯定需要一个锁,b/c 它是“多值”字典实现。如果添加了多个具有相同键的键值对,则它们将被包含在一个列表 (SimpleLinkList) 中。如果多个线程同时添加具有“相同键”的项目,那么没有锁实现将不是线程安全的。另外,我正在使用的锁是一个额外的锁(除了并发字典的内部锁),代码应该很慢,而不是比 AddOrUpdate 快。
  • 除了对字典中的所有列表使用一个锁之外,您还可以将列表本身用作锁,这样您就不会阻塞其他列表的操作,并发字典会完成它的工作。
猜你喜欢
  • 2016-10-22
  • 1970-01-01
  • 2011-05-17
  • 2019-10-02
  • 2010-10-30
  • 2023-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多