.net core 线程锁,互斥锁,自旋锁,混合锁

线程锁、互斥锁、自旋锁和混合锁是多线程编程中的重要概念,它们用于控制对共享资源的访问,避免数据竞争和不一致性。每种锁有其特定的适用场景和特点。我们来逐一解释它们,并进行比较。

1. 线程锁(Thread Lock)

线程锁的概念泛指任何用于同步多线程访问共享资源的机制。它的目的是确保在同一时刻只有一个线程可以访问资源,从而避免多个线程并发访问时发生数据竞争(race condition)或资源不一致。

线程锁通常是通过以下几种锁机制来实现的:

  • 互斥锁(Mutex)
  • 自旋锁(SpinLock)
  • 读写锁(ReadWriteLock)
  • 信号量(Semaphore)
  • 临界区(CriticalSection)

不同类型的锁有不同的实现方式和适用场景。

2. 互斥锁(Mutex)

互斥锁(Mutex)是一种最常见的同步原语,用于控制对共享资源的访问。它的基本思想是:如果一个线程已经获得了锁,其他线程必须等待,直到锁被释放,才能继续执行。

特点
  • 线程阻塞:当一个线程尝试获取互斥锁时,如果锁已被其他线程持有,线程会被挂起,直到锁可用为止。
  • 适用于长时间持有锁的情况:如果临界区代码较长,或线程会执行大量计算时,使用互斥锁能有效避免 CPU 资源的浪费。
  • 系统开销较高:挂起和恢复线程的操作比自旋等待更消耗系统资源。
示例:C# 中的 lock(实际上是基于 Monitor 的实现)
class Program
{
    private static readonly object lockObj = new object();
    private static int counter = 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: " + counter);
    }

    static void IncrementCounter()
    {
        lock (lockObj)  // 获取锁
        {
            counter++;  // 临界区
            Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} incremented counter to {counter}");
        }
    }
}

3. 自旋锁(SpinLock)

自旋锁是一种非常轻量级的同步机制,线程在尝试获取锁时,不会被挂起,而是会在一个循环中不断检查锁是否已经释放。线程会不断“自旋”并消耗 CPU 时间,直到获得锁。

特点
  • 忙等待:当一个线程请求自旋锁时,如果锁已经被其他线程持有,它会不断地检查锁是否已被释放,这种行为被称为“自旋”。
  • 适用于锁持有时间短的场景:当临界区代码执行时间非常短时,自旋锁可以避免线程挂起和恢复的高开销。
  • CPU 资源消耗较高:如果锁持有时间较长,多个线程可能会造成大量 CPU 资源的浪费。
示例:C# 中的 SpinLock
using System;
using System.Threading;

class Program
{
    private static SpinLock spinLock = new SpinLock();
    private static int counter = 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: " + counter);
    }

    static void IncrementCounter()
    {
        bool lockTaken = false;
        try
        {
            spinLock.Enter(ref lockTaken);  // 获取锁
            counter++;  // 临界区
            Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} incremented counter to {counter}");
        }
        finally
        {
            if (lockTaken)
                spinLock.Exit();  // 释放锁
        }
    }
}

4. 混合锁(Hybrid Lock)

混合锁是一种结合了互斥锁和自旋锁的锁机制,它通常用于试图在自旋锁和互斥锁之间根据具体情况进行切换,旨在提高多线程程序的效率。

混合锁的思想是:

  1. 自旋锁:在锁争用轻微、临界区代码执行时间短的情况下,使用自旋锁来减少线程挂起带来的性能开销。
  2. 互斥锁:如果自旋锁的时间过长,系统会自动切换为互斥锁,这样线程会被挂起,避免浪费过多 CPU 时间。
特点
  • 适应性强:混合锁通过平衡自旋和线程挂起的开销,避免在锁争用过于严重时造成资源浪费。
  • 自动调整:当争用变得严重时,混合锁会自动切换为互斥锁,而在争用轻微时,它会使用自旋来避免不必要的开销。
示例:C# 中没有直接的混合锁类,但可以通过自定义逻辑来实现类似功能。
using System;
using System.Threading;

class Program
{
    private static SpinLock spinLock = new SpinLock();
    private static object mutex = new object();
    private static int counter = 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: " + counter);
    }

    static void IncrementCounter()
    {
        bool lockTaken = false;
        try
        {
            // 尝试自旋锁
            if (!spinLock.TryEnter(100))  // 如果锁在 100ms 内未被获取
            {
                // 自旋失败,使用互斥锁
                lock (mutex)
                {
                    counter++;
                    Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} incremented counter to {counter}");
                }
            }
            else
            {
                // 获取自旋锁
                counter++;
                Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} incremented counter to {counter}");
            }
        }
        finally
        {
            if (lockTaken)
                spinLock.Exit();
        }
    }
}

自旋锁、互斥锁和混合锁的比较

特性/锁类型互斥锁(Mutex)自旋锁(SpinLock)混合锁(Hybrid Lock)
锁获取方式阻塞,线程被挂起自旋,线程忙等待锁根据锁的争用情况自旋或阻塞
适用场景锁持有时间长、锁竞争激烈的情况锁持有时间短、锁竞争轻的情况锁持有时间变化,既有自旋又有阻塞
性能开销较高,线程挂起与恢复开销较大较低,但如果竞争严重会浪费 CPU 资源较低,可以根据情况自动调整
适用性多线程竞争较高的场景低竞争、锁持有时间短的场景高竞争情况下动态选择锁类型
总结
  • 互斥锁 适用于锁持有时间较长、竞争激烈的场景,能有效避免资源争用,但可能会导致性能瓶颈。
  • 自旋锁 适用于锁持有时间非常短的场景,能够避免线程上下文切换的开销,但如果锁争用严重,可能会浪费大量 CPU 资源。
  • 混合锁 结合了自旋锁和互斥锁的优点,能根据锁争用情况动态选择自旋或挂起,从而提供更好的性能和适应性。

选择哪种锁取决于具体的应用场景和性能需求。在高并发、高竞争的环境中,混合锁可能是最优选择,而在低竞争或快速临界区的情况下,自旋锁也许是最合适的。

5.信号量

信号量(Semaphore) 是一种用于多线程编程中的同步机制,用于控制对共享资源的访问,特别是在资源数量有限时,它能够限制并发访问的线程数目。信号量通过维护一个计数器来管理线程的访问。线程在进入临界区之前,需要检查信号量的计数值,只有计数值大于零时,线程才能进入;当线程完成工作后,信号量的计数值会增加,允许其他线程进入。

信号量的基本概念
  • 计数器:信号量内部有一个整数计数器,表示可用的资源数量或允许并发执行的线程数。
  • P操作(或称为 Wait 或 Acquire):线程尝试减少信号量的计数器。如果信号量的计数器大于零,线程会成功进入临界区,计数器减一。如果计数器为零,线程会被阻塞,直到计数器大于零。
  • V操作(或称为 Signal 或 Release):线程在完成工作后,增加信号量的计数器,允许其他被阻塞的线程继续执行。
信号量的类型
  1. 计数信号量(Counting Semaphore):计数信号量的计数器值可以是任意非负整数,表示允许访问的资源数量或线程数。例如,如果有 5 个资源或 5 个线程可以并发执行,信号量的初始值为 5。每当一个线程获得资源时,计数器减一,释放资源时计数器加一。

  2. 二值信号量(Binary Semaphore):二值信号量是计数信号量的一种特殊情况,计数器值仅为 0 或 1。它常常用于控制一个线程的互斥访问,类似于互斥锁(Mutex)。二值信号量也被称为 互斥信号量,因为它的行为与互斥锁非常相似。

信号量的应用场景
  • 控制并发访问:信号量通常用于控制某些资源的并发访问,限制同时访问某些共享资源的线程数。例如,数据库连接池中的数据库连接数有限,信号量可以用来确保不超过最大连接数。

  • 限制资源数量:例如,线程池中只允许一定数量的线程同时运行任务,超出限制的线程会被阻塞,直到其他线程完成任务并释放资源。

  • 线程同步:在一些需要线程同步的场景中,信号量可以用来控制线程的执行顺序或协调多个线程之间的操作。

示例:C# 中使用信号量

假设我们有一个共享的数据库连接池,最多只允许 3 个线程同时访问数据库。我们可以使用信号量来限制并发访问。

using System;
using System.Threading;

class Program
{
    // 初始化信号量,最多允许 3 个线程并发访问
    private static Semaphore semaphore = new Semaphore(3, 3); 

    static void Main()
    {
        // 创建并启动 5 个线程
        for (int i = 0; i < 5; i++)
        {
            int threadId = i;
            Thread thread = new Thread(() => AccessDatabase(threadId));
            thread.Start();
        }
    }

    static void AccessDatabase(int threadId)
    {
        Console.WriteLine($"Thread {threadId} trying to access database...");

        // 尝试获取信号量
        semaphore.WaitOne();  // 如果信号量计数器大于 0,则进入临界区,计数器减 1

        try
        {
            Console.WriteLine($"Thread {threadId} is accessing the database.");
            Thread.Sleep(2000);  // 模拟数据库访问操作
            Console.WriteLine($"Thread {threadId} is done with the database.");
        }
        finally
        {
            // 释放信号量
            semaphore.Release();  // 释放资源,信号量计数器加 1
        }
    }
}
代码解释
  1. 信号量初始化:我们使用 Semaphore(3, 3) 来创建一个信号量,初始值为 3,表示最多允许 3 个线程同时访问共享资源(这里是模拟的数据库连接)。信号量的最大值也是 3,意味着最多只能有 3 个线程持有信号量。

  2. 线程尝试访问资源:每个线程在访问数据库之前调用 semaphore.WaitOne() 来尝试获取信号量。如果信号量的计数器大于 0,线程就能成功获得信号量并进入临界区,计数器减 1;如果计数器为 0,线程会被阻塞,直到其他线程释放信号量。

  3. 线程完成后释放信号量:在 finally 块中,线程完成工作后调用 semaphore.Release() 来释放信号量,允许其他线程访问共享资源。此时,信号量计数器加 1。

信号量与其他同步机制的比较
特性/机制信号量(Semaphore)互斥锁(Mutex)读写锁(ReadWriteLock)自旋锁(SpinLock)
锁粒度用于控制资源数量用于单个资源的互斥访问分别对读和写操作加锁轻量级的锁,用于短时间临界区
适用场景控制资源数量,限流,多线程并发访问防止多线程同时访问共享资源允许多个读者同时访问,写者互斥高并发且锁持有时间短的场景
阻塞方式阻塞线程或继续执行阻塞线程阻塞线程自旋,直到获得锁
优点控制并发数量,灵活高效确保资源的独占访问提高读取性能,允许并发读取轻量级,减少上下文切换的开销
总结

信号量是一种用于控制并发访问共享资源的同步工具,特别适用于资源数量有限的场景。它通过计数器来控制允许访问的线程数量,支持灵活的线程同步与调度。根据资源需求,信号量能够控制多个线程的并发执行,避免资源争用和冲突。

6.读写锁

读写锁是一种特殊类型的锁,它允许多个线程同时读取共享数据,但在写操作时,只能有一个线程进行写操作,而且在写操作时,其他线程不能进行读操作或写操作。读写锁旨在提高读操作多、写操作少的场景下的性能,尤其是在数据读取频繁而修改较少的情况下。

读写锁的工作原理
  • 读锁:多个线程可以同时持有读锁,只要没有线程持有写锁。读锁不会阻止其他线程获取读锁。
  • 写锁:写锁是排他性的,只有一个线程可以持有写锁。并且在持有写锁时,所有其他线程(无论是读锁还是写锁)都不能访问共享资源。
  • 读写锁的基本设计思想是:在没有写操作的情况下,允许多个线程并发读取;但是一旦有写操作开始,必须保证其他线程都无法访问资源。

C# 中的 ReaderWriterLockSlim

在 C# 中,ReaderWriterLockSlim 类提供了类似的功能,用于处理并发读写操作。

  • EnterReadLock():获取读锁,允许多个线程并发读取。
  • EnterWriteLock():获取写锁,排他性锁定,阻塞所有读写操作。
using System;
using System.Threading;

class Program
{
    static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
    static int sharedResource = 0;

    static void Main()
    {
        // 创建并发读取的线程
        Thread readThread1 = new Thread(() =>
        {
            rwLock.EnterReadLock();  // 获取读锁
            try
            {
                Console.WriteLine("Read Thread 1: " + sharedResource);
            }
            finally
            {
                rwLock.ExitReadLock();  // 释放读锁
            }
        });

        Thread readThread2 = new Thread(() =>
        {
            rwLock.EnterReadLock();  // 获取读锁
            try
            {
                Console.WriteLine("Read Thread 2: " + sharedResource);
            }
            finally
            {
                rwLock.ExitReadLock();  // 释放读锁
            }
        });

        // 创建写线程
        Thread writeThread = new Thread(() =>
        {
            rwLock.EnterWriteLock();  // 获取写锁
            try
            {
                sharedResource++;
                Console.WriteLine("Write Thread: " + sharedResource);
            }
            finally
            {
                rwLock.ExitWriteLock();  // 释放写锁
            }
        });

        // 启动线程
        readThread1.Start();
        readThread2.Start();
        writeThread.Start();
    }
}
读写锁的优势和适用场景
优势:
  1. 提高并发性能:当读操作频繁而写操作较少时,使用读写锁可以显著提高系统的并发性能。多个线程可以同时进行读操作,而无需等待锁的释放。
  2. 减少锁竞争:由于读操作不互斥,可以避免频繁的锁竞争,尤其在读操作占主导的场景中。
  3. 提供更细粒度的控制:相比传统的互斥锁(如 ReentrantLock),读写锁提供了更细粒度的锁机制,让读写操作更加高效。
适用场景:
  • 读多写少的场景:比如缓存、日志读取、数据库查询等,系统中的大多数操作是读操作,少量写操作。
  • 高并发读取:需要多个线程频繁读取共享资源,但写操作较少的应用(例如 Web 应用中的数据查询)。
  • 低并发写操作:确保在写操作发生时,不会有其他线程同时执行读操作,保持数据一致性。
需要注意的问题:
  1. 写操作可能会阻塞读操作:如果有大量的读操作而只有少数的写操作,写操作会造成较长时间的阻塞,导致性能下降。
  2. 死锁风险:在设计并发系统时,如果不小心使用了写锁嵌套或读锁嵌套,可能会导致死锁。
总结
  • 读写锁的设计旨在提高系统的并发性,特别是在读多写少的场景下。通过区分读锁和写锁,读写锁允许多个线程并行读操作,但写操作则是排他性的。
  • 它适用于需要大量读取操作且写操作相对较少的场景,可以有效减少线程之间的锁竞争,提高系统的性能。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Multisim更新:振幅调制器+解调器(含仿真程序+文档+原理图+PCB)

前言 继3年前设计的&#xff1a;Multisim&#xff1a;振幅调制器的设计&#xff08;含仿真程序文档原理图PCB&#xff09;&#xff0c;有读者表示已经不能满足新需求&#xff0c;需要加上新的解调器功能&#x1f602;&#x1f602;&#x1f602;&#xff0c;鸽了很久这里便安排…

BGP(Border Gateway Protocol)路由收集器

全球 BGP&#xff08;边界网关协议&#xff09;路由收集器的分布情况以及相关数据。以下是主要的信息解读&#xff1a; 地图标记&#xff1a; 每个绿色点代表一个路由收集器的位置。路由收集器分布在全球不同的地区&#xff0c;覆盖了五大区域&#xff1a; ARIN&#xff08;美…

【Rust自学】10.5. 生命周期 Pt.1:生命周期的定义与意义、借用检查器与泛型生命周期

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 10.5.1. 什么是生命周期 Rust的每个引用都有自己的生命周期&#xff0c;生命周期的作用是让引用保持有效&#xff0c;也可以说它是保持引…

Vue2: table加载树形数据的踩坑记录

table中需要加载树形数据,如图: 官网给了两个例子,且每个例子中的tree-props都是这么写的: :tree-props="{children: children, hasChildren: hasChildren}" 给我一种错觉,以为数据结构中要同时指定children和hasChildren字段,然而,在非懒加载模式下,数据结…

深入了解 SSL/TLS 协议及其工作原理

深入了解 SSL/TLS 协议及其工作原理 一. 什么是 SSL/TLS?二. SSL/TLS 握手过程三. SSL/TLS 数据加密与传输四. 总结 点个免费的赞和关注&#xff0c;有错误的地方请指出&#xff0c;看个人主页有惊喜。 作者&#xff1a;神的孩子都在歌唱 一. 什么是 SSL/TLS? 安全套接层&am…

sqlserver sql转HTMM邮件发送

通过sql的形式&#xff0c;把表内数据通过邮件的形式发送出去 declare title varchar(100) DECLARE stat_date CHAR(10),create_time datetime SET stat_dateCONVERT(char(10),GETDATE(),120) SET create_timeDATEADD(MINUTE,-20,GETDATE()) DECLARE xml NVARCHAR (max) DECLAR…

用QT实现 端口扫描工具1

安装在线QT&#xff0c;尽量是完整地自己进行安装&#xff0c;不然会少包 参考【保姆级图文教程】QT下载、安装、入门、配置VS Qt环境-CSDN博客 临时存储空间不够。 Windows系统通常会使用C盘来存储临时文件。 修改临时文件存储位置 打开系统属性&#xff1a; 右键点击“此电…

Selenium 自动化,如何下载正确的 ChromeDriver

在 Python 的 Selenium 自动化操作中&#xff0c;chromedriver 是不可或缺的驱动程序。没有正确安装对应版本的驱动&#xff0c;运行代码时常常会遇到报错问题&#xff0c;比如 “session not created: This version of ChromeDriver only supports Chrome version XX”。 今天…

泊松融合 实例2025

目录 例子1: 实现代码: 原作者代码: 本博客直接给出来最好的效果和源代码 参数说明: 效果不好,不推荐的参数:MONOCHROME_TRANSFER,NORMAL_CLONE 例子1: 目标图: 原图: 效果图: 实现代码: 坐标是要目标图上中心点坐标: import cv2if __na

前端如何从入门进阶到高级

在前端学习的道路上&#xff0c;我们将其划分为三个阶段&#xff1a;入门、实战和进阶。以下是各阶段的学习指南 一、入门阶段 在入门阶段&#xff0c;我们的目标是掌握前端的基本语法和知识&#xff0c;以便能够独立解决一些基础问题。这一阶段&#xff0c;我们建议通过视频…

Python爬虫基础——认识网页结构(各种标签的使用)

1、添加<div>标签的代码定义了两个区块的宽度和高度均为100px&#xff0c;边框的格式也相同&#xff0c;只是区块中显示的内容不同&#xff1b; 2、添加<ul>和<ol>标签分别用于定义无序列表和有序列表。<il>标签位于<ul>标签或<ol>标签之…

基于W2605C语音识别合成芯片的智能语音交互闹钟方案-AI对话享受智能生活

随着科技的飞速发展&#xff0c;智能家居产品正逐步渗透到我们的日常生活中&#xff0c;其中智能闹钟作为时间管理的得力助手&#xff0c;也在不断进化。基于W2605C语音识别与语音合成芯片的智能语音交互闹钟&#xff0c;凭借其强大的联网能力、自动校时功能、实时天气获取、以…

Python提取目标Json键值:包含子嵌套列表和字典

目标&#xff1a;取json中所有的Name、Age字典 思路&#xff1a;递归处理字典中直接包含子字典的情况&#xff0c; import jsondef find_targ_dicts(data,key1,key2):result {}if isinstance(data, dict):if key1 in data and key2 in data: # 第一层字典中包含key1和key2re…

你已经分清JAVA中JVM、JDK与JRE的作用和关系了吗?

你已经分清JAVA中JVM、JDK与JRE的作用和关系了吗&#xff1f; 一. JVM、JDK与JRE的关系二. JVM、JDK与JRE的作用2.1 什么是JVM&#xff1f;2.2 什么是JDK&#xff1f;2.3 什么是JRE&#xff1f; 前言 点个免费的赞和关注&#xff0c;有错误的地方请指出&#xff0c;看个人主页有…

深度学习blog-RAG构建高效生成式AI的优选路径

RAG&#xff08;Retrieval-Augmented Generation&#xff09; 随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;模型的性能和应用场景也不断扩展。其中&#xff0c;检索增强生成&#xff08;RAG, Retrieval-Augmented Generation&#xff09;模型作为一种新…

数据中台与数据治理服务方案[50页PPT]

本文概述了数据中台与数据治理服务方案的核心要点。数据中台作为政务服务数据化的核心&#xff0c;通过整合各部门业务系统数据&#xff0c;进行建模与加工&#xff0c;以新数据驱动政府管理效率提升与政务服务能力增强。数据治理则聚焦于解决整体架构问题&#xff0c;确保数据…

AI生成PPT,效率与创意的双重升级

AI生成PPT&#xff0c;效率与创意的双重升级&#xff01;在信息化高速发展的今天&#xff0c;我们的工作节奏被无限压缩&#xff0c;效率成为了衡量工作能力的重要指标。而制作PPT这种事&#xff0c;总是让人又爱又恨——既想做得出彩&#xff0c;又不想花费大量时间。现在有了…

【HF设计模式】05-单例模式

声明&#xff1a;仅为个人学习总结&#xff0c;还请批判性查看&#xff0c;如有不同观点&#xff0c;欢迎交流。 摘要 《Head First设计模式》第5章笔记&#xff1a;结合示例应用和代码&#xff0c;介绍单例模式&#xff0c;包括遇到的问题、采用的解决方案、以及达到的效果。…

嵌入式linux系统中QT信号与槽实现

第一:Qt中信号与槽简介 信号与槽是Qt编程的基础。因为有了信号与槽的编程机制,在Qt中处理界面各个组件的交互操作时变得更加直观和简单。 槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。 案例操作与实现: #ifndef …

php有两个数组map比较 通过id关联,number可能数量变化 比较他们之间增加修改删除

在PHP中&#xff0c;比较两个通过ID关联的数组&#xff0c;并确定它们之间的增加、修改和删除操作&#xff0c;你可以使用以下步骤&#xff1a; 创建两个数组&#xff1a;假设你有两个数组&#xff0c;分别表示“旧数据”和“新数据”。使用ID作为键&#xff1a;为了方便比较&a…