C#各种锁知识点

先上总结: 

锁类型特点适用场景优点缺点
自旋锁忙等待实现锁定,适合高并发极短时间锁定高并发、锁持有时间极短的场景(如多核 CPU 下的轻量级同步),仅限单进程多线程同步开销低,避免线程上下文切换,响应速度快忙等待消耗 CPU 资源,不适合长时间锁定,不能跨进程使用,高负载下性能恶化
互斥锁提供独占访问,阻塞等待,适合长时间锁定(C# 中对应Mutex需要跨线程或跨进程保护资源(如多程序共享打印机)阻塞等待不占用 CPU 资源,支持跨进程同步内核级对象,上下文切换开销高,可能导致死锁,使用复杂度高于lock
事件锁基于事件(Event)信号实现的线程同步机制,通过 “有信号 / 无信号” 控制线程阻塞 / 唤醒线程间 “等待 - 通知” 协作(如线程 A 完成任务后通知线程 B 执行)精准控制线程时序,支持一对一(AutoResetEvent)广播通知(ManualResetEvent)若信号未触发会永久阻塞;手动管理信号状态易出错;无信号时线程阻塞仍有调度开销
lock 关键字基于Monitor的代码块级同步机制,易用且自动处理异常简单的线程同步,保护共享资源(如多线程读写全局变量)语法简单,自动处理锁的获取与释放,减少死锁风险;托管层实现,开销低于互斥锁需选择合适锁对象(避免锁粒度太粗 / 太细);不能跨进程使用;线程阻塞时仍有上下文切换开销
信号量通过计数器控制同时访问资源的线程数量,分为系统级(Semaphore,支持跨进程)和轻量级(SemaphoreSlim,进程内,性能更优)需要限制并发线程数的场景(如控制同时写文件的线程数、数据库连接池管理、API 限流)灵活控制并发数(可设初始 / 最大计数);Semaphore支持跨进程同步,SemaphoreSlim进程内性能高效;避免资源过载(如磁盘 / 数据库压力)使用不当易死锁(如未释放名额);Semaphore是系统级对象开销略高;需手动确保Release调用(否则名额耗尽,阻塞后续线程);复杂场景下仍有调度开销
读写锁读与读并发、读与写互斥、写与写互斥(如ReaderWriterLockSlim多读少写场景(如缓存读写、配置文件访问)读操作并发效率高,大幅提升多线程读性能;支持锁升级(从读到写的安全过渡)写操作独占时阻塞所有读 / 写线程,写操作频繁时性能下降;升级锁逻辑复杂易死锁;默认不支持递归锁

粒度:锁的粒度是指锁定代码块的范围。如果锁定范围太大,会减少并发性,影响性能。 尽量减少锁定代码块的大小,只锁定必要的部分。

 自旋锁(Spin Lock)

  • 特点:当线程尝试获取锁时,如果锁已经被其他线程持有,线程会不停地检查锁是否可用(即“自旋”),直到获取到锁为止。
  • 优点:适用于锁定时间很短的场景,避免了线程的上下文切换,提高了性能。
  • 缺点:如果锁定时间较长,自旋锁会导致CPU资源的浪费,因为线程会一直占用CPU时间进行忙等待。
  • 使用场景:适用于锁定时间极短的代码段,且需要避免线程上下文切换的开销。
using System;
using System.Threading;

class SpinLockExample
{
    private volatile bool locked = false;

    public void Acquire()
    {
        while (true)
        {
            if (!locked)
            {
                locked = true;
                return;
            }
            Thread.SpinWait(1); // 自旋等待
        }
    }

    public void Release()
    {
        locked = false;
    }
}

class Program
{
    static SpinLockExample spinLock = new SpinLockExample();

    static void CriticalSection()
    {
        Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock");
        spinLock.Acquire();
        Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock");
        // Critical section
        Thread.Sleep(1000);
        Console.WriteLine($"{Thread.CurrentThread.Name} releasing lock");
        spinLock.Release();
    }

    static void Main()
    {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.Length; i++)
        {
            threads[i] = new Thread(CriticalSection);
            threads[i].Name = $"Thread {i+1}";
            threads[i].Start();
        }

        foreach (Thread thread in threads)
        {
            thread.Join();
        }
    }
}

互斥锁 (Mutex)

特点

  • 互斥锁(Mutex)提供对资源的独占访问。线程如果无法获取锁,会被阻塞并放入等待队列。
  • Mutex 可以用于跨线程和跨进程的锁定。
  • 由操作系统管理,能够确保在一个进程中获得锁时,其他进程无法获得同一个锁。

适用场景

  • 需要在多个线程或多个进程之间同步访问共享资源。
  • 需要锁定时间可能较长的操作。
  • 在本地应用程序或服务中使用。

优点

  • 提供跨进程的锁定机制。
  • 阻塞等待,不占用CPU资源。
  • 适合长时间锁定的操作。

缺点

  • 开销较高,涉及线程上下文切换。
  • 可能导致死锁,特别是在嵌套锁定的情况下。
using System;
using System.Threading;

class MutexExample
{
    static Mutex mutex = new Mutex();

    static void CriticalSection()
    {
        Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock");
        mutex.WaitOne();
        try
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock");
            // Critical section
            Thread.Sleep(1000);
        }
        finally
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} releasing lock");
            mutex.ReleaseMutex();
        }
    }

    static void Main()
    {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.Length; i++)
        {
            threads[i] = new Thread(CriticalSection);
            threads[i].Name = $"Thread {i+1}";
            threads[i].Start();
        }

        foreach (Thread thread in threads)
        {
            thread.Join();
        }
    }
}

事件锁

在多线程编程中,“事件锁” 通常指基于事件(Event)信号实现的线程同步机制,核心是通过 “有信号 / 无信号” 状态控制线程的阻塞与唤醒,实现线程间的协作(如 “等待 - 通知” 模式)。在 C# 中,主要通过 AutoResetEventManualResetEvent 及其轻量版 AutoResetEventSlimManualResetEventSlim 实现,它们本质是 “等待句柄(WaitHandle)”,而非传统意义上的 “锁”,但可用于类似 “锁” 的同步效果。

1. AutoResetEvent(自动复位事件)
  • 特点Set() 后自动复位为 “无信号”,仅唤醒一个等待线程(类似 “单次通知”)。
  • 适用场景:一对一的线程协作(如线程 A 完成任务后,通知线程 B 开始执行)。
using System;
using System.Threading;

class AutoResetEventDemo
{
    // 初始化:无信号状态(线程会阻塞)
    private static readonly AutoResetEvent _event = new AutoResetEvent(false);

    static void Main()
    {
        // 启动线程B,它会先阻塞等待信号
        new Thread(ThreadB).Start();

        // 线程A执行任务(模拟耗时)
        Console.WriteLine("线程A:开始执行任务...");
        Thread.Sleep(2000);
        Console.WriteLine("线程A:任务完成,发送信号");

        // 发送信号:将事件设为有信号,唤醒一个等待线程(线程B),然后自动复位为无信号
        _event.Set();
    }

    static void ThreadB()
    {
        Console.WriteLine("线程B:等待线程A的信号...");
        _event.WaitOne(); // 无信号时阻塞,收到信号后继续
        Console.WriteLine("线程B:收到信号,开始执行");
    }
}
2. ManualResetEvent(手动复位事件)
  • 特点Set() 后保持 “有信号” 状态,需手动调用 Reset() 复位;可唤醒所有等待线程(类似 “广播通知”)。
  • 适用场景:多线程等待同一信号(如 “准备工作完成后,所有工作线程同时开始执行”)。
using System;
using System.Threading;

class ManualResetEventDemo
{
    // 初始化:无信号状态
    private static readonly ManualResetEvent _event = new ManualResetEvent(false);

    static void Main()
    {
        // 启动3个工作线程,都等待信号
        for (int i = 0; i < 3; i++)
        {
            int threadId = i;
            new Thread(() => WorkerThread(threadId)).Start();
        }

        // 模拟准备工作
        Console.WriteLine("主线程:准备工作中...");
        Thread.Sleep(2000);
        Console.WriteLine("主线程:准备完成,发送信号(所有线程开始)");

        // 发送信号:设为有信号,所有等待线程被唤醒
        _event.Set();

        // (可选)一段时间后手动复位(如需再次控制)
        Thread.Sleep(1000);
        _event.Reset(); // 复位为无信号
    }

    static void WorkerThread(int id)
    {
        Console.WriteLine($"工作线程{id}:等待开始信号...");
        _event.WaitOne(); // 无信号时阻塞,有信号时继续
        Console.WriteLine($"工作线程{id}:收到信号,开始工作");
    }
}

 lock关键字

特点

  • lock 关键字基于 Monitor 实现,线程如果无法获取锁,会被阻塞并放入等待队列。
  • 适用于大多数普通的多线程同步场景,锁定时间可以较长。
  • 自动处理异常情况,确保在异常发生时释放锁。

优点

  • 使用方便,语法简洁。
  • 在等待锁时,线程不会消耗CPU资源。
  • 自动处理异常,确保锁的释放。

缺点

  • 有线程上下文切换的开销。
  • 需要选择合适的锁对象,避免锁定 this 或公共对象。
  • 不能跨进程使用。
class LockExample
{
    private readonly object lockObj = new object();

    public void CriticalSection()
    {
        lock (lockObj)
        {
            // Critical section
        }
    }
}

信号量(Semaphore)

在多线程程序中,线程并发访问共享资源(如全局变量、硬件接口、文件等)时,容易因 “资源竞争” 导致数据错乱、操作冲突,甚至软件崩溃。信号量(Semaphore) 是一种经典的线程同步机制,通过控制 “同时访问共享资源的线程数量”,避免并发冲突,从而防止崩溃。

  • Semaphore:系统级信号量,支持跨进程同步(如多个程序共享资源),但性能开销稍大。
  • SemaphoreSlim:轻量级信号量,仅支持进程内同步,性能更好,适合大多数单进程多线程场景(推荐优先使用)。

信号量通过一个 “计数器” 控制并发:

  • 初始计数:允许同时访问资源的初始线程数(如初始值 = 3,表示一开始有 3 个 “名额”)。
  • 最大计数:允许的最大并发线程数(名额上限)。
  • Wait 操作:线程申请名额(计数器 - 1),若计数器 = 0 则阻塞等待。
  • Release 操作:线程释放名额(计数器 + 1),唤醒等待的线程。

假设一个场景:3 个线程同时控制机械手移动,若并发调用移动函数可能导致崩溃。用信号量限制同一时间只有 1 个线程能调用移动函数。

using System;
using System.Threading;

class RobotControl
{
    // 信号量:初始值=1(只允许1个线程同时访问),最大并发数=1
    private static SemaphoreSlim _semaphore = new SemaphoreSlim (initialCount: 1, maximumCount: 1);

    // 机械手移动函数(共享资源操作)
    public static void MoveRobot(int x, int y)
    {
        // 1. 等待信号量(P操作):获取访问权,若被占用则阻塞
        _semaphore.Wait();

        try
        {
            // 核心操作:实际控制机械手移动(假设此过程不能并发)
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:移动机械手到({x},{y})");
            Thread.Sleep(1000); // 模拟移动耗时
        }
        finally
        {
            // 2. 释放信号量(V操作):无论是否出错,都必须释放,否则会导致死锁
            _semaphore.Release();
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:释放访问权");
        }
    }

    static void Main()
    {
        // 启动3个线程,同时尝试控制机械手
        for (int i = 0; i < 3; i++)
        {
            new Thread(() => MoveRobot(10 * i, 20 * i)).Start();
        }
    }
}

Semaphore 用法(跨进程同步)

Semaphore 是系统级信号量,可通过 “名称” 在多个进程间共享(如两个程序共享同一硬件资源)。

程序 A(进程 1)

using System;
using System.Threading;

class SemaphoreDemo_ProcessA
{
    static void Main()
    {
        // 命名信号量:名称"SharedSemaphore"用于跨进程识别,初始计数=1,最大计数=1
        using (var semaphore = new Semaphore(initialCount: 1, maximumCount: 1, name: "SharedSemaphore"))
        {
            Console.WriteLine("程序A:等待硬件访问权限...");
            semaphore.WaitOne(); // 申请权限

            try
            {
                Console.WriteLine("程序A:获得权限,开始操作硬件...");
                Thread.Sleep(5000); // 模拟操作硬件
            }
            finally
            {
                semaphore.Release();
                Console.WriteLine("程序A:释放权限");
            }
        }
    }
}

程序 B(进程 2)

using System;
using System.Threading;

class SemaphoreDemo_ProcessB
{
    static void Main()
    {
        // 打开同名信号量(跨进程共享)
        using (var semaphore = Semaphore.OpenExisting("SharedSemaphore"))
        {
            Console.WriteLine("程序B:等待硬件访问权限...");
            semaphore.WaitOne(); // 若程序A未释放,会阻塞

            try
            {
                Console.WriteLine("程序B:获得权限,开始操作硬件...");
                Thread.Sleep(3000);
            }
            finally
            {
                semaphore.Release();
                Console.WriteLine("程序B:释放权限");
            }
        }
    }
}

执行效果

  • 先运行程序 A,立即获得权限;
  • 同时运行程序 B,会阻塞在 WaitOne(),直到程序 A 释放权限(5 秒后);
  • 程序 A 释放后,程序 B 立即获得权限,实现跨进程的资源互斥。

读写锁ReaderWriterLock

在多线程编程中,当读操作远多于写操作时(如缓存数据、配置信息访问),使用普通锁(如lock)会导致读线程之间互相阻塞,降低并发效率。读写锁(Reader-Writer Lock) 专门解决这种场景:它允许多个读线程同时访问资源,但写线程需要独占资源(读与写、写与写互斥),从而在保证线程安全的同时提高性能。

读写锁的核心是 “读写分离”,定义两种锁状态:

  • 读锁(共享锁):多个读线程可同时获取,互不阻塞(读与读兼容)。
  • 写锁(排他锁):仅允许一个写线程获取,且会阻塞所有读线程和其他写线程(读与写、写与写互斥)。

场景:一个共享字典(_cache)作为缓存,大量线程读取数据,偶尔有线程更新数据。用 ReaderWriterLockSlim 控制访问:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

class CacheDemo
{
    // 共享缓存(键值对存储)
    private static readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
    // 读写锁:默认非递归(不允许同一线程重复获取锁)
    private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();

    // 读操作:从缓存获取数据(允许多线程并发)
    public static string GetFromCache(string key)
    {
        // 获取读锁(多个读线程可同时进入)
        _rwLock.EnterReadLock();
        try
        {
            if (_cache.TryGetValue(key, out string value))
            {
                Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:读取到 {key} = {value}");
                return value;
            }
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:未找到 {key}");
            return null;
        }
        finally
        {
            // 释放读锁(必须在finally中执行,确保释放)
            _rwLock.ExitReadLock();
        }
    }

    // 写操作:更新缓存(独占资源,阻塞所有读和写)
    public static void UpdateCache(string key, string value)
    {
        // 获取写锁(仅当前线程可进入,其他读/写线程阻塞)
        _rwLock.EnterWriteLock();
        try
        {
            _cache[key] = value;
            Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}:更新 {key} = {value}");
        }
        finally
        {
            // 释放写锁
            _rwLock.ExitWriteLock();
        }
    }

    static void Main()
    {
        // 初始化缓存
        UpdateCache("name", "Alice");

        // 启动5个读线程(并发读取)
        for (int i = 0; i < 5; i++)
        {
            Task.Run(() => GetFromCache("name"));
        }

        // 延迟1秒,确保读线程执行后,启动1个写线程
        Thread.Sleep(1000);
        Task.Run(() => UpdateCache("name", "Bob"));

        // 再启动5个读线程(读取更新后的值)
        Thread.Sleep(1000);
        for (int i = 0; i < 5; i++)
        {
            Task.Run(() => GetFromCache("name"));
        }

        Console.ReadLine();
    }
}

执行效果:

线程3:读取到 name = Alice
线程4:读取到 name = Alice
线程5:读取到 name = Alice
线程6:读取到 name = Alice
线程7:读取到 name = Alice
// 写线程获取写锁,阻塞所有读线程,完成更新
线程8:更新 name = Bob
// 写锁释放后,新的读线程并发读取更新后的值
线程9:读取到 name = Bob
线程10:读取到 name = Bob
线程11:读取到 name = Bob
线程12:读取到 name = Bob
线程13:读取到 name = Bob

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值