当不可变集合比并发更可取时

xne*_*neg 5 .net c# concurrency concurrent-collections immutable-collections

最近阅读了关于不可变集合的信息。当读取操作的执行频率高于写入操作时,建议将它们用作线程安全的读取操作。

然后我想测试读取性能ImmutableDictionaryConcurrentDictionary. 这是这个非常简单的测试(在 .NET Core 2.1 中):

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace ImmutableSpeedTests
{
    class Program
    {
        public class ConcurrentVsImmutable
        {
            public int ValuesCount;
            public int ThreadsCount;

            private ImmutableDictionary<int, int> immutable = ImmutableDictionary<int, int>.Empty;
            private ConcurrentDictionary<int, int> concurrent = new ConcurrentDictionary<int, int>();

            public ConcurrentVsImmutable(int valuesCount, int threadsCount)
            {
                ValuesCount = valuesCount;
                ThreadsCount = threadsCount;
            }

            public void Setup()
            {
                // fill both collections. I don't measure time cause immutable is filling much slower obviously.
                for (var i = 0; i < ValuesCount; i++)
                {
                    concurrent[i] = i;
                    immutable = immutable.Add(i, i);
                }
            }

            public async Task<long> ImmutableSum() => await Sum(immutable);

            public async Task<long> ConcurrentSum() => await Sum(concurrent);

            private async Task<long> Sum(IReadOnlyDictionary<int, int> dic)
            {
                var tasks = new List<Task<long>>();

                // main job. Run multiple tasks to sum all values.
                for (var i = 0; i < ThreadsCount; i++)
                    tasks.Add(Task.Run(() =>
                    {
                        long x = 0;
                        foreach (var key in dic.Keys)
                        {
                            x += dic[key];
                        }
                        return x;
                    }));

                var result = await Task.WhenAll(tasks.ToArray());
                return result.Sum();
            }
        }

        static void Main(string[] args)
        {
            var test = new ConcurrentVsImmutable(1000000, 4);

            test.Setup();

            var sw = new Stopwatch();

            sw.Start();
            var result = test.ConcurrentSum().Result;
            sw.Stop();

            // Convince that the result of the work is the same
            Console.WriteLine($"Concurrent. Result: {result}. Elapsed: {sw.ElapsedTicks}.");

            sw.Reset();
            sw.Start();
            result = test.ImmutableSum().Result;
            sw.Stop();

            Console.WriteLine($" Immutable. Result: {result}. Elapsed: {sw.ElapsedTicks}.");

            Console.ReadLine();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以运行此代码。以滴答为单位的经过时间会不时不同,但 by 花费的ConcurrentDictionary时间比 by 少几倍ImmutableDictionary

这个实验让我很尴尬。我做错了吗?如果我们有并发,使用不可变集合的原因是什么?他们什么时候更可取?

Aka*_*ava 5

不可变集合不能替代并发集合。它们旨在减少内存消耗的方式,它们肯定会更慢,这里的权衡是使用更少的内存,从而使用更少的 n 操作来做任何事情。

我们通常将集合复制到其他集合以实现持久化状态的不变性。来看看是什么意思

 var s1 = ImmutableStack<int>.Empty;
 var s2 = s1.Push(1);
 // s2 = [1]

 var s3 = s2.Push(2);
 // s2 = [1]
 // s3 = [1,2]

 // notice that s2 has only one item, it is not modified..

 var s4 = s3.Pop(ref var i);

 // s2 = [1];
 // still s2 has one item...
Run Code Online (Sandbox Code Playgroud)

请注意, s2 始终只有一项。即使删除所有项目。

所有数据在内部存储的方式是一棵巨大的树,您的集合指向一个分支,该分支具有代表树的初始状态的后代。

我认为性能无法与目标完全不同的并发收集相匹配。

在并发集合中,您拥有一个可供所有线程访问的集合副本

在不可变集合中,您实际上拥有一棵树的独立副本,导航该树总是代价高昂

它在事务系统中很有用,如果事务必须回滚,收集状态可以保留在提交点中。