C# Interlocked 类( 二 )


C# Interlocked 类

文章插图
可以看到这个函数没有 32 位(int)类型的重载,为什么要单独为 64 位的 long/ulong 类型单独提供原子性读取操作符呢?
这是因为CPU有 32 位处理器和 64 位处理器,在 64 位处理器上,寄存器一次处理的数据宽度是 64 位,因此在 64 位处理器和 64 位操作系统上运行的程序 , 可以一次性读取 64 位数值 。
但是在 32 位处理器和 32 位操作系统情况下,long/ulong 这种数值,则要分成两步操作来进行 , 分别读取 32 位数据后,再合并在一起,那显然就会出现多线程情况下的并发问题 。
因此这里提供了原子性的方法来应对这种情况 。
C# Interlocked 类

文章插图
这里底层同样用了 CompareExchange 操作来保证原子性 , 参数这里就给了两个0,可以兼容如果原值是 0 则写入 0,如果原值非 0 则不写入 , 返回原值 。
__sync_val_compare_and_swap 函数在写入新值之前,读出旧值,当且仅当旧值与存储中的当前值一致时,才把新值写入存储
【关于性能】多线程下实现原子性操作方式有很多种,我们一定会关心在不同场景下 , 不同方法间的性能问题,那么我们简单来对比下 Interlocked 类提供的方法和 lock 关键字的性能对比
我们同样用线程池调度50个Task(内部可能线程重用),分别执行 200000 次自增运算
public static void IncreamentPerformance(){//lock methodvar locker = new object();var stopwatch = new Stopwatch();stopwatch.Start();var j1 = 0;Task.WaitAll(Enumerable.Range(0, 50).Select(t =>Task.Run(() =>{for (int i = 0; i < 200000; i++){lock (locker){j1++;}}})).ToArray());Console.WriteLine($"Monitor lock,result={j1},elapsed={stopwatch.ElapsedMilliseconds}");stopwatch.Restart();//Increment methodvar j2 = 0;Task.WaitAll(Enumerable.Range(0, 50).Select(t =>Task.Run(() =>{for (int i = 0; i < 200000; i++){Interlocked.Increment(ref j2);}})).ToArray());stopwatch.Stop();Console.WriteLine($"Interlocked.Increment , result={j2},elapsed={stopwatch.ElapsedMilliseconds}");}运算结果
C# Interlocked 类

文章插图
可以看到,采用 Interlocked 类中的自增函数,性能比 lock 方式要好一些
虽然这里看起来性能要好,但是不同的业务场景要针对性思考,采用恰当的编码方式,不要一味追求性能
我们简单分析下造成执行时间差异的原因
我们都知道,使用lock(底层是Monitor类),在上述代码中会阻塞线程执行,保证同一时刻只能有一个线程执行 j1++ 操作,因此能保证操作的原子性 , 那么在多核CPU下,也只能有一个CPU核心在执行这段逻辑 , 其他核心都会等待或执行其他事件,线程阻塞后,并不会一直在这里傻等,而是由操作系统调度执行其他任务 。由此带来的代价可能是频繁的线程上下文切换,并且CPU使用率不会太高,我们可以用分析工具来印证下 。
Visual Studio 自带的分析工具 , 查看线程使用率
C# Interlocked 类

文章插图
使用 Process Explorer 工具查看代码执行过程中上下文切换数
C# Interlocked 类

文章插图

C# Interlocked 类

文章插图
可以大概估计出 , 采用 lock(Monitor)同步自增方式,上下文切换 243
那么我们用同样的方式看下底层用 CAS 函数执行自增的开销
Visual Studio 自带的分析工具,查看线程使用率
C# Interlocked 类

文章插图
使用 Process Explorer 工具查看代码执行过程中上下文切换数
C# Interlocked 类

文章插图

C# Interlocked 类

文章插图
可以大概估计出,采用 CAS 自增方式,上下文切换 220
可见,不论使用什么技术手段,线程创建太多都会带来大量的线程上下文切换
这个应该是和测试的代码相关
两者比较大的区别在CPU的使用率上,因为 lock 方式会造成线程阻塞,因此不会所有的CPU核心同时参与运算 , CPU在当前进程上使用率不会太高,但 cas 方式CPU在自己的时间分片内并没有被阻塞或重新调度 , 而是不停地执行比较替换的动作(其实这种场景算是无用功,不必要的负开销),造成CPU使用率非常高 。

推荐阅读