C# Interlocked 类使用详解

news2025/1/27 4:41:43

总目录


前言

在多线程编程中,确保多个线程对共享资源的安全访问是一个关键挑战。C# 提供了多种同步机制来处理并发问题,其中 System.Threading.Interlocked 类提供了一种轻量级的方法来进行原子操作。它允许您执行一些常见的增量、减量、交换等操作,并保证这些操作是线程安全的,即在同一时刻只有一个线程可以修改共享数据。


一、Interlocked 相关概念介绍

1. 原子操作

1) 定义

原子操作指的是那些在执行过程中不可被中断的操作。也就是说,一旦原子操作开始执行,它会在不被其他线程干扰的情况下完整执行完毕,要么全部执行成功,要么全部不执行,不会出现执行到一半被其他线程打断的情况。在多线程环境下,原子操作能够保证数据的一致性和完整性。

2) 原理

原子操作的实现通常依赖于硬件层面的支持。现代 CPU 提供了一些特殊的指令,例如比较并交换(Compare - And - Swap, CAS)指令。这些指令可以在一个时钟周期内完成对内存的读取、比较和写入操作,并且在执行过程中不会被其他 CPU 核心的操作打断。Interlocked 类就是利用这些硬件指令来实现原子操作的,它会调用底层的 CPU 指令,确保操作的原子性。

3)作用

在多线程环境中,多个线程可能同时访问和修改共享资源。如果没有适当的同步机制,就可能出现数据竞争的问题,导致数据不一致或程序出现不可预期的结果。原子操作可以避免这种情况的发生,因为它保证了对共享资源的操作是线程安全的。例如,多个线程同时对一个共享的计数器进行递增操作,如果不使用原子操作,可能会出现多个线程同时读取到相同的计数器值,然后各自加 1 后写回,最终导致计数器的值增加的结果不符合预期。而使用原子操作,就能确保每次递增操作都是独立且完整的。

2. 数据竞争

1) 定义

数据竞争指的是在多线程环境中,两个或多个线程同时访问共享数据,并且至少有一个线程对该共享数据进行写操作,同时又没有使用合适的同步机制来协调这些访问,从而导致数据的不一致性或程序产生不可预期的结果。

2) 原因

  • 线程调度的不确定性:操作系统负责线程的调度,它会根据自身的调度算法在不同线程之间切换执行。这就使得多个线程对共享数据的访问顺序变得不可预测,可能会出现一个线程正在修改数据时,另一个线程同时读取或修改该数据的情况。
  • 缺乏同步机制:如果在多线程程序中没有使用合适的同步机制(如锁、原子操作等)来控制对共享数据的访问,各个线程就会随意地访问和修改共享数据,进而引发数据竞争。

3) 示例

以下是一个简单的示例,展示了数据竞争的情况:

using System;
using System.Threading;

class Program
{
    private static int sharedCounter = 0;

    static void Main()
    {
        // 创建两个线程
        Thread thread1 = new Thread(IncrementCounter);
        Thread thread2 = new Thread(IncrementCounter);

        // 启动线程
        thread1.Start();
        thread2.Start();

        // 等待两个线程执行完毕
        thread1.Join();
        thread2.Join();

        // 输出最终的计数器值
        Console.WriteLine($"Final counter value: {sharedCounter}");

		//第一次输出结果: Final counter value: 1226406
		//第二次输出结果: Final counter value: 1551244
		// ...
		// 会发现每次输出的结果都不一样
    }

    static void IncrementCounter()
    {
        for (int i = 0; i < 100_0000; i++)
        {
            sharedCounter++;
        }
    }
}

在这个示例中,两个线程同时对 sharedCounter 进行递增操作。sharedCounter++ 实际上包含了三个步骤:读取 sharedCounter 的值、将该值加 1、再将结果写回 sharedCounter。由于线程调度的不确定性,可能会出现以下情况:

  • 线程 1 读取 sharedCounter 的值为 10。
  • 线程 2 也读取 sharedCounter 的值,此时由于线程 1 还未完成写操作,所以线程 2 读到的值也是 10。
  • 线程 1 将 10 加 1 得到 11,并写回 sharedCounter。
  • 线程 2 同样将 10 加 1 得到 11,并写回 sharedCounter。

这样一来,两次递增操作实际上只让 sharedCounter 的值增加了 1,而不是 2,最终导致计数器的值比预期的要小,这就是数据竞争的具体表现。

4)危害

  • 数据不一致:数据竞争会使共享数据的值变得不可预测,不同线程可能会看到不同版本的数据,导致程序的状态混乱。
  • 程序崩溃:在某些情况下,数据竞争可能会导致程序出现未定义行为,进而引发程序崩溃或产生难以调试的错误。
  • 结果不可重复:由于数据竞争的发生依赖于线程调度的不确定性,所以程序的运行结果可能每次都不一样,这给程序的调试和测试带来了极大的困难。

5)解决办法

  • 使用锁机制:可以使用 lock 关键字(在 C# 中)或其他同步原语(如 Mutex、Semaphore 等)来确保同一时间只有一个线程能够访问共享数据。例如,将上述示例中的 IncrementCounter 方法修改如下:
private static readonly object lockObject = new object();

static void IncrementCounter()
{
    for (int i = 0; i < 100_0000; i++)
    {
        lock (lockObject)
        {
            sharedCounter++;
        }
    }
}

// 输出结果: Final counter value: 2000000
  • 使用原子操作:对于一些简单的共享数据操作(如整数的递增、递减等),可以使用 Interlocked 类提供的原子操作方法,这些方法可以确保操作的原子性,避免数据竞争。例如:
static void IncrementCounter()
{
    for (int i = 0; i < 100_0000; i++)
    {
        Interlocked.Increment(ref sharedCounter);
    }
}
// 输出结果: Final counter value: 2000000

3. Interlocked 类

通过以上内容和相关示例,我们知道为什么要使用 Interlocked 类?因为 Interlocked 类提供的一些原子操作方法,这些方法可以确保操作的原子性,避免多线程场景下的数据竞争问题。

下面我们就具体详细的了解一下 Interlocked
在这里插入图片描述

Interlocked 类位于 System.Threading 命名空间,是一组静态方法的集合。它提供的方法能保证对共享变量的操作以原子方式执行,即这些操作在执行过程中不会被其他线程中断。这有效避免了多线程环境下的数据竞争问题,确保数据的一致性和完整性。

Interlocked 用于执行无锁(lock-free)的原子操作。这些方法可以直接在共享变量上工作,而不需要显式的锁定机制(如 lock 语句),从而减少了死锁和其他同步问题的风险。由于它们是基于硬件级别的原子指令实现的,因此通常比传统的锁更高效。

假设我们有一个计数器需要在多个线程间安全地递增。我们可以利用 Interlocked.Increment 来避免竞态条件

class Counter
{
    private long count = 0;

    public void Increment()
    {
        Interlocked.Increment(ref count);
    }

    public long GetCount()
    {
        return count;
    }
}

// 在多个线程中调用Increment方法
var counter = new Counter();
Parallel.For(0, 1000, i => counter.Increment());
Console.WriteLine(counter.GetCount()); // 应输出1000

二、Interlocked 常用方法

1. Interlocked.Add

  • 介绍:该方法接受两个参数,一个是要修改的目标变量的引用,另一个是要添加到目标变量上的数值。它同样以原子方式执行加法并返回新的值。
  • 功能:以原子操作的方式将指定值加到共享变量上,并返回相加后的结果。
  • 语法:public static int Add(ref int location1, int value); 此方法有针对不同数值类型的重载,如long、float、double等。
  • 示例:
using System;
using System.Threading;

class Program
{
    private static int sharedValue = 0;

    static void Main()
    {
        Thread thread1 = new Thread(() =>
        {
            for (int i = 0; i < 1000; i++)
            {
                Interlocked.Add(ref sharedValue, 1);
            }
        });

        Thread thread2 = new Thread(() =>
        {
            for (int i = 0; i < 1000; i++)
            {
                Interlocked.Add(ref sharedValue, 1);
            }
        });

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine($"Final value: {sharedValue}");
    }
}
  • 说明:在上述代码中,两个线程同时尝试对sharedValue进行累加操作。如果不使用Interlocked.Add,由于线程调度的不确定性,可能会导致数据竞争,最终结果可能不是预期的 2000。而Interlocked.Add确保了每次累加操作的原子性,保证了结果的正确性。

2. Interlocked.Increment 和 Interlocked.Decrement

  • 介绍:这两个方法分别用于将指定位置的整数值加1或减1。它们返回更新后的值,并且保证此操作是原子性的,不会受到其他线程的影响。
  • 功能:Interlocked.Increment 以原子操作的方式递增共享变量,并返回操作后的值;Interlocked.Decrement 则以原子操作的方式递减共享变量,并返回操作后的值。
  • 语法:public static int Increment(ref int location);public static int Decrement(ref int location); 同样有针对不同数值类型的重载。
  • 示例:
using System;
using System.Threading;

class Program
{
    private static int counter = 0;

    static void Main()
    {
        Thread incrementThread = new Thread(() =>
        {
            for (int i = 0; i < 500; i++)
            {
                Interlocked.Increment(ref counter);
            }
        });

        Thread decrementThread = new Thread(() =>
        {
            for (int i = 0; i < 500; i++)
            {
                Interlocked.Decrement(ref counter);
            }
        });

        incrementThread.Start();
        decrementThread.Start();

        incrementThread.Join();
        decrementThread.Join();

        Console.WriteLine($"Final counter value: {counter}");
    }
}
  • 说明:该示例展示了如何在多线程环境中安全地对计数器进行递增和递减操作。Interlocked.Increment 和 Interlocked.Decrement 保证了这些操作不会因线程切换而出现数据不一致的情况。

3. Interlocked.Exchange

  • 介绍:Exchange 方法会用新值替换目标变量的旧值,并返回原来的值。这有助于在线程之间传递信息而不必担心竞争条件。

  • 功能:以原子操作的方式用新值替换共享变量的旧值,并返回旧值。

  • 语法:public static int Exchange(ref int location1, int value); 有多种数据类型的重载。

  • 示例:

using System;
using System.Threading;

class Program
{
    private static int sharedNumber = 10;

    static void Main()
    {
        Thread thread = new Thread(() =>
        {
            int oldValue = Interlocked.Exchange(ref sharedNumber, 20);
            Console.WriteLine($"Old value: {oldValue}");	//Old value: 10
        });

        thread.Start();
        thread.Join();

        Console.WriteLine($"New value: {sharedNumber}");	//New value: 20
    }
}
  • 说明:在此示例中,线程通过Interlocked.Exchange方法将sharedNumber的值替换为 20,并获取原来的值。这个操作是原子的,不会受到其他线程干扰。

4. Interlocked.CompareExchange

  • 介绍:这是最强大的一个方法,它尝试将目标变量设置为新值,但仅当其当前值等于预期值时才会成功。如果匹配失败,则保持不变,并返回实际的旧值。这对于实现自旋锁或其他复杂的同步逻辑非常有用。

  • 功能:以原子操作的方式比较共享变量的值与给定值,如果相等,则用新值替换共享变量的值,并返回共享变量的原始值。常用于实现无锁算法。

  • 语法:public static int CompareExchange(ref int location1, int value, int comparand); 同样支持多种数据类型的重载。

  • 示例:

using System;
using System.Threading;

class Program
{
    private static int sharedValue = 5;

    static void Main()
    {
        int comparisonValue = 5;
        int newValue = 10;
		// 如果sharedValue等于comparisonValue,则将其设置为newValue
        int result = Interlocked.CompareExchange(ref sharedValue, newValue, comparisonValue);

        if (result == comparisonValue)
        {
            Console.WriteLine($"Value was successfully updated to {newValue}");
        }
        else
        {
            Console.WriteLine($"Value was not updated. Current value: {sharedValue}");
        }
    }
}
  • 说明:在上述代码中,Interlocked.CompareExchange方法首先比较sharedValue与comparisonValue,如果相等,则将sharedValue更新为newValue。通过返回值可以判断更新是否成功,这在多线程环境下实现复杂同步逻辑时非常有用。

三、使用须知

  • 性能考量:虽然Interlocked操作是原子的,但在高并发场景下,频繁调用可能会带来一定的性能开销。在实际应用中,应根据具体需求权衡使用。

  • 适用场景:Interlocked主要适用于简单数据类型的原子操作,如计数器、标志位翻转等。对于复杂对象的同步,可能需要使用其他同步机制,如lock语句、Monitor类等。

  • 无锁优势:Interlocked 的原子操作避免了传统锁带来的上下文切换开销,对于高频率的小型操作来说性能优越。

  • 硬件支持:Interlocked 操作依赖于CPU提供的原子指令集,所以在现代多核处理器上表现良好。

  • 最小化作用域:尽量缩小 Interlocked 操作的作用范围,只保护真正需要同步的部分代码。

  • 组合使用:虽然 Interlocked 提供了基本的原子操作,但在某些情况下可能需要与 Monitor, Semaphore, 或者 ReaderWriterLockSlim 等高级同步原语结合起来使用。

四、Interlocked类和lock关键字区别

在 C# 多线程编程中,Interlocked类和lock关键字都用于处理线程安全问题,但它们在功能、实现方式、适用场景等方面存在明显区别:

1. 功能特性

  • Interlocked类:提供的是原子操作,确保对简单数据类型(如int、long等)的特定操作(如加减、交换、比较交换)在多线程环境下以原子方式执行,不会被其他线程中断。例如Interlocked.Add方法,在多个线程同时对一个共享整数变量进行累加时,能保证每次累加操作的完整性,避免数据竞争。
  • lock关键字:用于锁定一个对象,在同一时间只允许一个线程进入被锁定的代码块,从而保证这段代码在多线程环境下的线程安全性。它可以保护任何类型的共享资源,不仅限于简单数据类型。

2. 实现方式

  • Interlocked类:基于硬件指令实现原子操作,通常依赖于 CPU 提供的特殊指令,如cmpxchg(比较并交换指令)等。这些硬件指令保证了操作的原子性,操作系统和编译器会确保这些指令在执行过程中不会被打断。
  • lock关键字:是基于Monitor类实现的语法糖。lock语句在进入代码块时获取对象的锁,离开代码块时释放锁。当一个线程获取了锁,其他线程试图进入被锁定的代码块时,会被阻塞,直到锁被释放。

3. 适用场景

  • Interlocked类:适用于对简单数据类型进行简单的、独立的原子操作场景。例如,在多线程环境下统计某个事件发生的次数,使用Interlocked.Increment就非常合适。由于其操作的原子性是基于硬件指令,性能开销相对较小,在高并发场景下,如果只需要对简单数据进行这类原子操作,Interlocked类是较好的选择。
  • lock关键字:适用于保护复杂的代码块或对复杂对象的操作。当需要保证一段代码中多个操作的原子性,或者需要对多个共享资源进行协调访问时,lock关键字更为合适。比如,在多线程环境下对一个共享的集合进行读写操作,为了避免数据不一致,就可以使用lock关键字来锁定集合对象,确保同一时间只有一个线程能访问该集合。

4. 性能表现

  • Interlocked类:由于基于硬件指令,在对简单数据类型的原子操作上性能较高。特别是在高并发场景下,频繁的Interlocked操作带来的性能开销相对较小,因为它不需要像lock那样进行复杂的锁获取和释放操作。
  • lock关键字:虽然能保护复杂的代码逻辑,但由于涉及到线程的阻塞和唤醒,在高并发场景下,如果锁的竞争激烈,会导致性能下降。因为被阻塞的线程需要等待锁的释放,这期间会消耗系统资源,并且线程上下文的切换也会带来额外的开销。

结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
Interlocked 类
Interlocked 类,原子操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2283184.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VS Code i18n国际化组件代码code显示中文配置 i18n ally

VUE项目做i18n国际化之后&#xff0c;代码中的中文都变成了code这时的代码就会显得非常难读&#xff0c;如果有一个插件能把code转换成中文显示就好了 vscode插件搜索“i18n ally” 在项目根文件夹下创建文件&#xff1a;.vscode/settings.json settings.json 内容如下 {"…

MySQL日志详解——日志分类、二进制日志bin log、回滚日志undo log、重做日志redo log

文章目录 一、前言1.1 MySQL体系结构1.2 MySQL日志分类1.3 其他几种日志1.3.1 查询日志1.3.2 慢查询日志1.3.3 错误日志 二、bin log 二进制日志2.1 bin log简介2.2 binlog日志格式2.3 日志删除2.4 写入/刷盘机制 三、undo log 回滚日志3.1 undo log简介3.2 隐藏字段 —— 事务…

一文速通stack和queue的理解与使用

CSTL之stack和queue 1.stack1.1.stack的基本概念1.2.stack的接口 2.queue2.1.queue的基本概念2.2.queue的接口 3.priority_queue3.1.priority_queue的基本概念3.2.priority_queue的接口3.3.仿函数 4.容器适配器5.deque5.1.deque的简单了解5.2.deque的优缺点 &#x1f31f;&…

关于CAN(FD)转以太网详细介绍

一、功能描述 CANFD 完全向下兼容 CAN &#xff0c;以下统称 CAN(FD) 。 SG-CAN(FD)NET-210 是一款用来把 CANFD 总线数据转为网口数据的设备。 网口支持 TCP Sever 、 TCP Client 、 UDP Sever 、 UDP Client 四种模式。 可以通过软件配置和 Web 网页配置。 两路…

orbbec 奥比中光相机单目及多目调用方式python代码

这篇文章会介绍使用orbbec相机的一些常用代码梯子 orbbec 奥比中光Astra相机单目及多目调用方式编译sdk调用单相机调用多相机orbbec 奥比中光Astra相机单目及多目调用方式 Orbbec相机是一个专注于深度感知和计算机视觉应用的设备,通常用于3D扫描、手势识别、增强现实(AR)以及…

力扣hot100-->滑动窗口、贪心

你好呀&#xff0c;欢迎来到 Dong雨 的技术小栈 &#x1f331; 在这里&#xff0c;我们一同探索代码的奥秘&#xff0c;感受技术的魅力 ✨。 &#x1f449; 我的小世界&#xff1a;Dong雨 &#x1f4cc; 分享我的学习旅程 &#x1f6e0;️ 提供贴心的实用工具 &#x1f4a1; 记…

PBFT算法

在我的博客中对于RAFT算法也有详细的介绍&#xff0c;raft算法包含三种角色&#xff0c;分别是&#xff1a;跟随者&#xff08; follower &#xff09;&#xff0c;候选人&#xff08;candidate &#xff09;和领导者&#xff08; leader &#xff09;。集群中的一个节点在某一…

跨境电商代购系统独立站深度分享

在全球化日益加深的今天&#xff0c;跨境电商代购系统独立站作为一种新兴的电子商务模式&#xff0c;正逐渐成为连接全球消费者与优质商品的重要桥梁。本文将详细介绍跨境电商代购系统独立站的基本功能以及技术实现的重难点&#xff0c;以期为相关从业者提供一些有价值的参考和…

携程旅行 登录分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向分析 所有加密流程基本一样就说…

西藏酥油茶:高原上的醇香温暖

西藏酥油茶:高原上的醇香温暖 在西藏高原,有一种饮品,它不仅滋养了一代又一代的藏民,还承载着丰富的文化与历史,它就是西藏酥油茶。酥油茶,藏语称为“恰苏玛”,意为搅动的茶,是藏族人民日常生活中不可或缺的一部分,更是待客、祭祀等活动中的重要礼仪物品。 历史与文化渊源 酥…

刷题总结 回溯算法

为了方便复习并且在把算法忘掉的时候能尽量快速的捡起来 刷完回溯算法这里需要做个总结 回溯算法的适用范围 回溯算法是深度优先搜索&#xff08;DFS&#xff09;的一种特定应用&#xff0c;在DFS的基础上引入了约束检查和回退机制。 相比于普通的DFS&#xff0c;回溯法的优…

Antd React Form使用Radio嵌套多个Select和Input的处理

使用Antd React Form使用Radio会遇到嵌套多个Select和Input的处理&#xff0c;需要多层嵌套和处理默认事件和冒泡&#xff0c;具体实现过程直接上代码。 实现效果布局如下图 代码 <Formname"basic"form{form}labelWrap{...formItemLayoutSpan(5, 19)}onFinish{on…

Linux(Centos、Ubuntu) 系统安装jenkins服务

该文章手把手演示在Linux系统下如何安装jenkins服务、并自定义jenkins数据文件位置、以及jenkins如何设置国内镜像源加速&#xff0c;解决插件下载失败问题 安装方式&#xff1a;war包安装 阿里云提供的war下载源地址&#xff1a;https://mirrors.aliyun.com/jenkins/war/?s…

【基于无线电的数据通信链】Link 11 仿真测试

〇、废话 Link 11 仿真测试 涉及多个方面&#xff0c;包括信号仿真、协议模拟、数据链路层的仿真以及网络性能评估等。Link 11 是一种基于 HF&#xff08;高频&#xff09; 或 UHF&#xff08;超高频&#xff09; 波段的无线通信协议&#xff0c;主要用于军事通信系统中。为了…

VScode 开发 Springboot 程序

1. 通过maven创建springboot程序 输入 mvn archetype:generate 选择模板&#xff0c;一般默认选择为第 7 种方式&#xff1b; 选择之后&#xff0c;一般要你填写如下内容&#xff1a; groupId: 组织名称&#xff1b;artifactId: 项目名称&#xff1b;version: 版本&#xff0…

深入MapReduce——引入

引入 前面我们已经深入了HDFS的设计与实现&#xff0c;对于分布式系统也有了不错的理解。 但HDFS仅仅解决了海量数据存储和读写的问题。要想让数据产生价值&#xff0c;一定是需要从数据中挖掘出价值才行&#xff0c;这就需要我们拥有海量数据的计算处理能力。 下面我们还是…

springfox-swagger-ui 3.0.0 配置

在3.0中&#xff0c;访问地址URL变了。 http://地址:端口/项目名/swagger-ui/ SpringBoot maven项目引入 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>3.0.0</version> </…

如何解压7z文件?8种方法(Win/Mac/手机/网页端)

7z 文件是一种高效的压缩文件格式&#xff0c;由 7 - Zip 软件开发者所采用。它运用独特的压缩算法&#xff0c;能显著缩小文件体积&#xff0c;便于存储与传输各类数据&#xff0c;像软件安装包、大型资料集等。但要使用其中内容&#xff0c;就必须解压&#xff0c;因为处于压…

Spring Boot 邂逅Netty:构建高性能网络应用的奇妙之旅

一、引言 在当今数字化时代&#xff0c;构建高效、可靠的网络应用是开发者面临的重要挑战。Spring Boot 作为一款强大的 Java 开发框架&#xff0c;以其快速开发、简洁配置和丰富的生态支持&#xff0c;深受广大开发者喜爱。而 Netty 作为高性能、异步的网络通信框架&#xff…

[STM32 - 野火] - - - 固件库学习笔记 - - -十一.电源管理系统

一、电源管理系统简介 电源管理系统是STM32硬件设计和系统运行的基础&#xff0c;它不仅为芯片本身提供稳定的电源&#xff0c;还通过多种电源管理功能优化功耗、延长电池寿命&#xff0c;并确保系统的可靠性和稳定性。 二、电源监控器 作用&#xff1a;保证STM32芯片工作在…