Skip to main content
 首页 » 编程设计

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

2024年01月26日29daizhj

最近阅读了有关不可变集合的内容。 当读取操作比写入更频繁地执行时,建议将它们用作读取的线程安全。

然后我想测试读取性能 ImmutableDictionary 与 ConcurrentDictionary。这是这个非常简单的测试(在 .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(); 
        } 
    } 
} 

您可以运行此代码。以滴答表示的耗时会不时不同,但 ConcurrentDictionary 花费的时间比 ImmutableDictionary 花费的时间少几倍。

这个实验让我很尴尬。我做错了吗?如果我们有并发,为什么要使用不可变集合?什么时候更合适?

请您参考如下方法:

不可变集合不能替代并发集合。它们旨在减少内存消耗的方式必然会更慢,这里的权衡是使用更少的内存,从而使用更少的 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... 

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

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

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

在并发集合中,所有线程都访问一个集合副本。

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

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