虽然更新并发字典中的值最好锁定字典或值

While updating a value in concurrent dictionary is better to lock dictionary or value

我正在对从 TryGet 获得的值执行两次更新我想知道哪个更好?

选项 1:仅锁定值?

if (HubMemory.AppUsers.TryGetValue(ConID, out OnlineInfo onlineinfo))
{
    lock (onlineinfo)
    {
        onlineinfo.SessionRequestId = 0;
        onlineinfo.AudioSessionRequestId = 0;
        onlineinfo.VideoSessionRequestId = 0;
    }
}

选项 2:锁定整个词典?

if (HubMemory.AppUsers.TryGetValue(ConID, out OnlineInfo onlineinfo))
{
    lock (HubMemory.AppUsers)
    {
        onlineinfo.SessionRequestId = 0;
        onlineinfo.AudioSessionRequestId = 0;
        onlineinfo.VideoSessionRequestId = 0;
    }
}

如果您只需要 锁定 字典 值,例如确保同时设置 3 个值。那么你锁定什么引用类型并不重要,只要它一个引用类型,它是同一个 实例 ,其他所有需要读取或修改这些值的东西也在同一个 上被锁定 实例.

您可以阅读更多关于 Microsoft CLR 实现 如何处理 锁定 以及锁如何以及为什么与 引用类型 这里

如果你试图与字典值具有内部一致性,也就是说,如果你不仅试图保护字典的内部一致性,而且字典中对象的设置和阅读。那么你的 lock 根本不合适。

您需要在 lock around 整个语句(包括 TryGetValue)和所有其他地方放置一个 lock您将值添加到字典或 read/modify。再次重申,您锁定的对象并不重要,只要它是一致的即可。

注1:通常使用专用实例来锁定(即一些实例化object)根据您的需要静态或实例成员,因为您搬起石头砸自己脚的可能性较小。

注2可以[=53=的方法还有很多] 在这里实现线程安全,取决于您的需要,如果您对陈旧的值感到满意,您是否需要每一盎司的性能,以及您是否在最小锁编码方面有一定程度,以及您付出了多少努力和天生的安全性想要融入其中。这完全取决于您和您的解决方案.

第一个选项(锁定字典的条目)效率更高,因为它不太可能引起对锁的严重争用。为此,两个线程应尝试同时更新同一个条目。第二种选择(锁定整个字典)很可能在大量使用时造成争用,因为即使两个线程试图同时更新不同的条目,它们也会同步。

第一个选项也更符合首先使用 ConcurrentDictionary<K,V> 的精神。如果您要锁定整个字典,您不妨使用普通的 Dictionary<K,V> 代替。关于这个困境,你可能会觉得这个问题很有趣:

我要提出一些不同的建议。

首先,您应该在字典中存储不可变类型以避免大量线程问题。事实上,任何代码都可以修改字典中任何项目的内容,只需从中检索项目并更改其属性即可。

其次,ConcurrentDictionary 提供了 TryUpdate() 方法,允许您更新字典中的值而无需实现显式锁定。

TryUpdate()需要三个参数:要更新的项的key,更新后的项和从字典中得到然后更新的原始项。

TryUpdate() 然后通过将字典中当前的值与您传递给它的原始值进行比较来检查原始值是否未更新。只有当它是 SAME 时,它才会使用新值和 return true 实际更新它。否则它 returns false 不更新它。

这使您能够检测并适当地响应某些其他线程在您更新项目时更改了您正在更新的项目的值的情况。您可以忽略它(在这种情况下,第一个更改优先)或重试直到成功(在这种情况下,最后一个更改优先)。你做什么取决于你的情况。

请注意,这需要您的类型实现 IEquatable<T>,因为 ConcurrentDictionary 使用它来比较值。

这是一个示例控制台应用程序,演示了这一点:

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    sealed class Test: IEquatable<Test>
    {
        public Test(int value1, int value2, int value3)
        {
            Value1 = value1;
            Value2 = value2;
            Value3 = value3;
        }

        public Test(Test other) // Copy ctor.
        {
            Value1 = other.Value1;
            Value2 = other.Value2;
            Value3 = other.Value3;
        }

        public int Value1 { get; }
        public int Value2 { get; }
        public int Value3 { get; }

        #region IEquatable<Test> implementation (generated using Resharper)

        public bool Equals(Test other)
        {
            if (other is null)
                return false;

            if (ReferenceEquals(this, other))
                return true;

            return Value1 == other.Value1 && Value2 == other.Value2 && Value2 == other.Value3;
        }

        public override bool Equals(object obj)
        {
            return ReferenceEquals(this, obj) || obj is Test other && Equals(other);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (Value1 * 397) ^ Value2;
            }
        }

        public static bool operator ==(Test left, Test right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(Test left, Test right)
        {
            return !Equals(left, right);
        }

        #endregion
    }

    static class Program
    {
        static void Main()
        {
            var dict = new ConcurrentDictionary<int, Test>();

            dict.TryAdd(0, new Test(1000, 2000, 3000));
            dict.TryAdd(1, new Test(4000, 5000, 6000));
            dict.TryAdd(2, new Test(7000, 8000, 9000));

            Parallel.Invoke(() => update(dict), () => update(dict));
        }

        static void update(ConcurrentDictionary<int, Test> dict)
        {
            for (int i = 0; i < 100000; ++i)
            {
                for (int attempt = 0 ;; ++attempt)
                {
                    var original  = dict[0];
                    var modified  = new Test(original.Value1 + 1, original.Value2 + 1, original.Value3 + 1);
                    var updatedOk = dict.TryUpdate(1, modified, original);

                    if (updatedOk) // Updated OK so don't try again.
                        break;     // In some cases you might not care, so you would never try again.

                    Console.WriteLine($"dict.TryUpdate() returned false in iteration {i} attempt {attempt} on thread {Thread.CurrentThread.ManagedThreadId}");
                }
            }
        }
    }
}

那里有很多样板代码来支持 IEquatable<T> 实现并支持不变性。

幸运的是,C# 9 引入了 record 类型,这使得不可变类型更容易实现。这是使用 record 的相同示例控制台应用程序。请注意,record 类型是不可变的,并且还会为您实现 IEquality<T>

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace System.Runtime.CompilerServices // Remove this if compiling with .Net 5
{                                         // This is to allow earlier versions of .Net to use records.
    class IsExternalInit {}
}

namespace Demo
{
    record Test(int Value1, int Value2, int Value3);

    static class Program
    {
        static void Main()
        {
            var dict = new ConcurrentDictionary<int, Test>();

            dict.TryAdd(0, new Test(1000, 2000, 3000));
            dict.TryAdd(1, new Test(4000, 5000, 6000));
            dict.TryAdd(2, new Test(7000, 8000, 9000));

            Parallel.Invoke(() => update(dict), () => update(dict));
        }

        static void update(ConcurrentDictionary<int, Test> dict)
        {
            for (int i = 0; i < 100000; ++i)
            {
                for (int attempt = 0 ;; ++attempt)
                {
                    var original  = dict[0];

                    var modified  = original with
                    {
                        Value1 = original.Value1 + 1,
                        Value2 = original.Value2 + 1,
                        Value3 = original.Value3 + 1
                    };

                    var updatedOk = dict.TryUpdate(1, modified, original);

                    if (updatedOk) // Updated OK so don't try again.
                        break;     // In some cases you might not care, so you would never try again.

                    Console.WriteLine($"dict.TryUpdate() returned false in iteration {i} attempt {attempt} on thread {Thread.CurrentThread.ManagedThreadId}");
                }
            }
        }
    }
}

请注意 record Testclass Test 相比要短得多,即使它提供相同的功能。 (另请注意,我添加了 class IsExternalInit 以允许记录与 .Net 5 之前的 .Net 版本一起使用。如果您使用的是 .Net 5,则不需要。)

最后,请注意您 不需要 使 class 不可变。如果您的 class 是可变的,我为第一个示例发布的代码将完美运行;它只是不会阻止其他代码破坏东西。


附录 1:

您可能会查看输出并想知道为什么在 TryUpdate() 失败时进行了如此多的重试。您可能希望它只需要重试几次(取决于同时尝试修改数据的线程数)。答案很简单,Console.WriteLine() 花费的时间太长,很有可能是其他线程在我们写入控制台时再次更改了字典中的值。

我们可以稍微更改代码以仅打印循环外的尝试次数(修改第二个示例):

static void update(ConcurrentDictionary<int, Test> dict)
{
    for (int i = 0; i < 100000; ++i)
    {
        int attempt = 0;
        
        while (true)
        {
            var original  = dict[1];

            var modified  = original with
            {
                Value1 = original.Value1 + 1,
                Value2 = original.Value2 + 1,
                Value3 = original.Value3 + 1
            };

            var updatedOk = dict.TryUpdate(1, modified, original);

            if (updatedOk) // Updated OK so don't try again.
                break;     // In some cases you might not care, so you would never try again.

            ++attempt;
        }

        if (attempt > 0)
            Console.WriteLine($"dict.TryUpdate() took {attempt} retries in iteration {i} on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

通过此更改,我们发现重试次数显着下降。这表明在 TryUpdate() 次尝试之间尽量减少在代码中花费的时间的重要性。


附录 2:

正如下面 Theodor Zoulias 所指出的,您也可以使用 ConcurrentDictionary<TKey,TValue>.AddOrUpdate(),如下例所示。这可能是一种更好的方法,但它稍微难以理解:

static void update(ConcurrentDictionary<int, Test> dict)
{
    for (int i = 0; i < 100000; ++i)
    {
        int attempt = 0;
        
        dict.AddOrUpdate(
            1,                        // Key to update.
            key => new Test(1, 2, 3), // Create new element; won't actually be called for this example.
            (key, existing) =>        // Update existing element. Key not needed for this example.
            {
                ++attempt;

                return existing with
                {
                    Value1 = existing.Value1 + 1,
                    Value2 = existing.Value2 + 1,
                    Value3 = existing.Value3 + 1
                };
            }
        );

        if (attempt > 1)
            Console.WriteLine($"dict.TryUpdate() took {attempt-1} retries in iteration {i} on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}