先上总结:
| 锁类型 | 特点 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 自旋锁 | 忙等待实现锁定,适合高并发极短时间锁定 | 高并发、锁持有时间极短的场景(如多核 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# 中,主要通过 AutoResetEvent、ManualResetEvent 及其轻量版 AutoResetEventSlim、ManualResetEventSlim 实现,它们本质是 “等待句柄(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
1106

被折叠的 条评论
为什么被折叠?



