C# Interlocked 类

【前言】

在日常开发工作中,我们经常要对变量进行操作,例如对一个int变量递增++。在单线程环境下是没有问题的,但是如果一个变量被多个线程操作,那就有可能出现结果和预期不一致的问题。

例如:

static void Main(string[] args)
{
    var j = 0;
    for (int i = 0; i < 100; i++)
    {
        j++;
    }
    Console.WriteLine(j);
    //100
}

在单线程情况下执行,结果一定为100,那么在多线程情况下呢?

static void Main(string[] args)
{
    var j = 0;
    var t1 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            j++;
        }
    });
    var t2 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            j++;
        }
    });
    Task.WaitAll(t1, t2);
    Console.WriteLine(j);
    //82869 这个结果是随机的,和每个线程执行情况有关
}

我们可以看到,多线程情况下并不能保证执行正确,我们也将这种情况称为 “非线程安全”

这种情况下我们可以通过加锁来达到线程安全的目的

static void Main(string[] args)
{
    var locker = new object();
    var j = 0;
    var t1 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            lock (locker)
            {
                j++;
            }
        }
    });
    var t2 = Task.Run(() =>
    {
        for (int i = 0; i < 50000; i++)
        {
            lock (locker)
            {
                j++;
            }
        }
    });
    Task.WaitAll(t1, t2);
    Console.WriteLine(j);
    //100000 这里是一定的
}

加锁的确能解决上述问题,那么有没有一种更加轻量级,更加简洁的写法呢?

那么,今天我们就来认识一下 Interlocked 类

【Interlocked 类下的方法】

Increment(ref int location)

Increment 方法可以轻松实现线程安全的变量自增

///
/// thread safe increament
///
public static void Increament()
{
    var j = 0;

    Task.WaitAll(
        Enumerable.Range(0, 50)
        .Select(t =>
            Task.Run(() =>
            {
                for (int i = 0; i < 2000; i++)
                {
                    Interlocked.Increment(ref j);
                }
            }
        ))
        .ToArray()
        );

    Console.WriteLine($"multi thread increament result={j}");
    //result=100000
}

看到这里,我们一定好奇这个方法底层是怎么实现的?

我们通过ILSpy反编译查看源码:

首先看到 Increment 方法其实是通过调用 Add 方法来实现自增的

C# Interlocked 类

再往下看, Add 方法是通过 ExchangeAdd 方法来实现原子性的自增,因为该方法返回值是增加前的原值,因此返回时增加了本次新增的,结果便是相加的结果,当然 location1 变量已经递增成功了,这里只是为了友好地返回增加后的结果。

C# Interlocked 类

我们再往下看

C# Interlocked 类

这个方法用 [MethodImpl(MethodImplOptions.InternalCall)] 修饰,表明这里调用的是 CLR 内部代码,我们只能通过查看源码来继续学习。

我们打开 dotnetcore 源码:https://github.com/dotnet/corefx

找到 Interlocked 中的 ExchangeAdd 方法

C# Interlocked 类

可以看到,该方法用循环不断自旋赋值并检查是否赋值成功(CompareExchange返回的是修改前的值,如果返回结果和修改前结果是一致,则说明修改成功)

我们继续看内部实现

C# Interlocked 类

C# Interlocked 类

内部调用 InterlockedCompareExchange 函数,再往下就是直接调用的C++源码了

C# Interlocked 类

C# Interlocked 类

在这里将变量添加 volatile 修饰符,阻止寄存器缓存变量值(关于volatile不在此赘述),然后直接调用了C++底层内部函数 __sync_val_compare_and_swap 实现原子性的比较交换操作,这里直接用的是 CPU 指令进行原子性操作,性能非常高。

相同机制函数

Increment 函数机制类似, Interlocked 类下的大部分方法都是通过 CompareExchange 底层函数来操作的,因此这里不再赘述

  • Add 添加值
  • CompareExchange 比较交换
  • Decrement 自减
  • Exchange 交换
  • And 按位与
  • Or 按位或
  • Read 读64位数值

public static long Read(ref long location)

Read 这个函数着重提一下

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 method

    var 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&#xFF0C;result={j1},elapsed={stopwatch.ElapsedMilliseconds}");

    stopwatch.Restart();

    //Increment method

    var 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&#xFF0C;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使用率非常高。

【总结】

简单来说,Interlocked 类提供的方法给我们带来了方便快捷操作字段的方式,比起使用锁同步的编程方式来说,要轻量不少,执行效率也大大提高。但是该技术并非银弹,一定要考虑清楚使用的场景后再决定使用,比如服务器web应用下,多线程执行大量耗费CPU的运算,可能会严重影响应用吞吐量。虽然表面看起来执行这个单一的任务效率高一些(代价是CPU全部扑在这个任务上,无法响应其他任务),其实在我们的测试中,总共执行了 10000000 次运算,这种场景应该是比较极端的,而且在web应用场景下,用 lock 的方式响应时间也没有达到不能容忍的程度,但是用 lock 的好处是cpu可以处理其他用户请求的任务,极大提高了吞吐量。

我们建议在竞争较少的场景,或者不需要很高吞吐量的场景下(简单说是CPU时间不那么宝贵的场景下)我们可以用 Interlocked 类来保证操作的原子性,可以适当提升性能。而在竞争非常激烈的场景下,一定不要用 Interlocked 来处理原子性操作,改用 lock 方式会好很多。

【源码地址】

https://github.com/sevenTiny/CodeArts/blob/master/CSharp/Demo.CSharp/System_Threading/Interlocked_Test.cs

Original: https://www.cnblogs.com/7tiny/p/16833391.html
Author: 7tiny
Title: C# Interlocked 类

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/799466/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • python 不保留float最后的0

    [NO56、删除链表中的重复结点(不保留重复点,很好的题目) 56、删除链表中的重复结点,不保留重复点 很好的题目在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复…

    Python 2023年5月25日
    089
  • pandas两个数据结构Series与DataFrame使用

    seires 对象 1.创建一个series对象 Series() 语法:s=pd.Series(data,index=index) 参数说明: data:表示数据,支持Pytho…

    Python 2023年8月7日
    033
  • python代码报错No module named numpy问题

    1 一般在”控制面板+cmd”中安装numpy 在命令行窗口中输入 “pip install numpy” 此时安装的numpy并不…

    Python 2023年8月23日
    054
  • python程序的分支结构(专题)

    python程序的分支结构 前言 程序的分支结构分为三种,分别是单分支结构,二分支结构,多分支结构。同时需要掌握条件判断及组合,程序的异常处理。 一、单分支结构 根据判断条件结果而…

    Python 2023年8月24日
    058
  • Kaggle练习赛Spaceship Titantic数据探索(上)

    Kaggle练习赛Spaceship Titantic数据探索(上) kaggle上的练习赛,自己对训练集数据做的一个简单的数据探索。 网址 数据特征描述: PassengerId…

    Python 2023年8月7日
    074
  • Python从入门到精通(第五篇,基础篇)

    今天介绍一下pygame库,用于创建画板。 Pygame Pygame是一组跨平台的Python模块, 用于创建视频游戏。 它由旨在与Python编程语言一起使用的计算机图形和声音…

    Python 2023年9月24日
    038
  • python管理系统(大作业)

    一、🔎源代码 二、💯代码讲解 user_list = [ {‘name’: ‘张三’, ‘phone’: ‘123’, ‘wx’: ‘321’}, {‘name’: ‘李四’, ‘…

    Python 2023年8月2日
    079
  • apply函数分享

    apply函数是pandas中极其好用的一个函数,它可以对dataframe在行或列方向上进行批量化处理,从而大大简化数据处理的过程。 apply函数的基本形式: DataFram…

    Python 2023年8月21日
    035
  • Matplotlib绘图颜色

    文章目录 一、基本颜色 二、颜色对照表 三、渐变色 四、混色 五、颜色与十六进制对应 一、基本颜色 r——red b——blue c——cyan g——green k——black…

    Python 2023年9月7日
    079
  • 盘点10个冷门Python库,原来Python还能实现这些功能?

    目录 👉 1 PrettyErrors 👉 2 Rich 👉 3 Dear PyGui 👉 4 HummingBird 👉 5 HiPlot 👉 6 Norfair 👉 7 Geo…

    Python 2023年10月8日
    079
  • Python魔法方法之__iter__

    定义 __iter__方法后 下面的例子简单实现一个 range(n) from numpy import iterable class MyList: def __init__(…

    Python 2023年8月23日
    042
  • Python+Pychram+pytest环境搭建

    安装 python 官网下载地址:https://www.python.org/downloads/ 目前已经更新到了3.9.5 Python3.6安装步骤 去命令行输入:pyth…

    Python 2023年9月11日
    063
  • redash二次开发和制作镜像

    简介: 上一篇文章,我们简单的测试了一下服务器环境和docker基础镜像。并没有涉及我们自己编写的flask python程序。 现在,我们就要把我们自己的flask程序,放进do…

    Python 2023年8月15日
    047
  • 用python刷算法–堆排序算法

    堆排序算法流程 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到…

    Python 2023年6月3日
    071
  • hbuilder项目和页面在手机上运行|vue项目在手机上运行|django项目在手机上运行

    hbuilder项目和页面在手机上运行|vue项目在手机上运行|django项目在手机上运行 一、HBuilder项目或页面在手机上运行: hbuilder项目在电脑浏览器的运行网…

    Python 2023年8月5日
    057
  • scrapy命令明细:全局命令

    接下来我们来一一介绍scrapy命令有哪些,其实灰常少,也就十四五个,在这十四五个中,常用的就纳么两三个而已,如: scrapy startproject(创建项目)、scrapy…

    Python 2023年10月5日
    055
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球