C#--- 锁总结
1.按照功能分类
1. 互斥锁
1 Lock(Monitor类)
学习的最全最佳的方式是微软文档
微软官方解释是:提供一种机制,用于在不同线程之间的代码区域中实现相互排斥。
1.1 使用
private readonly object _lockObj = new object();void CriticalSection()
{lock (_lockObj){// 临界区代码}
}
1.2 特点
同一时刻,只能有一个线程进入临界区
1.4 总结
lock(this) 锁定 当前实例对象,如果有多个类实例的话,lock锁定的只是当前类实例,对其它类实例无影响。
lock(typeof(Model))锁定的是model类的所有实例。
lock(obj)锁定的对象是全局的私有化静态变量。外部无法对该变量进行访问。
lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
所以,lock的结果好不好,还是关键看锁的谁,如果外边能对这个谁进行修改,lock就失去了作用。所以一般情况下,使用私有的、静态的并且是只读的对象。
1.5 注意
1、lock的是必须是引用类型的对象,string类型除外。
2、lock推荐的做法是使用静态的、只读的、私有的对象。
3、保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。
不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。
总结:
避免对不同的共享资源使用相同的锁对象实例,因为这可能导致死锁或锁争用。特别要避免使用以下实例作为锁对象:
因为调用者也可能锁定这个
- type 实例,因为它们可以通过 typeof 操作符或反射获得
- string 实例,包括 string 字面量,因为它们可能被拘禁
- 保持锁的时间尽可能短,以减少锁竞争
2 Monitor
Monitor.Enter(_lockObj);
try
{// 临界区代码
}
finally
{Monitor.Exit(_lockObj);
}
提供比 lock 更精细的控制(如 TryEnter)
2.1 属性方法
属性方法 | 描述 |
---|---|
Enter(Object) | 在指定对象上获取排他锁。 |
Exit(Object) | 释放指定对象上的排他锁 |
TryEnter(Object) | 试图获取指定对象的排他锁。 |
TryEnter(Object, Boolean) | 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。 |
Wait(Object) | 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 |
Pulse | 通知等待队列中的线程锁定对象状态的更改 |
PulseAll | 通知所有的等待线程对象状态的更改 |
TryEnter 和 Enter 都是获取锁
TryEnter(object obj, TimeSpan timeout, ref bool lockTaken)
不阻塞,当在指定的时间间隔内,未试图获取指定对象的排他锁,则 lockTaken 为false,否则为True
程序直接往后执行。
用TryEnter() 方法可以避免死锁的发生。
Monitor.TryEnter(Object,Int32)。
设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false,也就是说,在1秒中后,lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁
2.2示例代码
以申请会议室为例,假如公司有一个会议室,只能开一个会议,其它想要开的会议要么等待会议结束开始会议,要么走人。那么会议室就是一个零界区资源,每一次只允许一个会议占用。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Study01
{internal class Program{static void Main(string[] args){// TestLock();TestLock1();Console.ReadLine();}/// <summary>/// lock的基本使用/// </summary>public static void TestLock1(){lockTest lockTest = new lockTest();for (int i = 0; i < 20; i++){Thread thread2 = new Thread(new ThreadStart(lockTest.AskClass));thread2.IsBackground = false;thread2.Start();}}}public class lockTest{private static readonly object _lock = new object();private static readonly object _lock1 = new object();public int _count = 0;public void Increment(){lock (_lock){_count++;Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}Count: {_count}");}}public void MonitrorIncrement(){bool locktoken = false;try{// Monitor.TryEnter(_lock1, ref locktoken);_count++;Thread.Sleep(100); // 模拟一些工作Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}Count: {_count}");}finally{if (locktoken)Monitor.Exit(_lock1);}}public void AskClass(){bool locktoken = false;Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}申请会议室{DateTime.Now}");try{Monitor.TryEnter(_lock1, TimeSpan.FromSeconds(2), ref locktoken);// Monitor.Enter(_lock1, ref locktoken);if (locktoken){Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}开始会议");Thread.Sleep(5000); // 模拟使用教室的时间}else{Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}未申请到会议室{DateTime.Now}");}}finally{if (locktoken){Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}离开会议室{DateTime.Now}");Monitor.Exit(_lock1);}}}}
}
简单测试
在主程序中我创建一个lockTest类型对象的实例,并且 创建20个线程,每个线程都去申请会议室
这里输出,这里Lock 是私有的,只读的,保证线程外安全,由于这20个线程都是执行的是 同一个实例,所以就算这里lock 不是静态的,保证了Lock 对象是同一个。
分析: 多个请求申请会议室 线程5 开始申请会议,在1S内 成功获取到锁,并进入会议,等到5s会议结束,释放锁。其它线程在会开始申请会议时,在1S内未获取到锁,故未申请到会议。
若使用:Monitor.Enter(_lock1, ref locktoken);
则 线程4 申请会议室,获取到锁,然后开始会议,5s, 结束会议,释放锁
线程5 申请会议室时,此时 4 已经开始了会议,锁还没释放,在等待锁释放
线程6 申请会议室时,此时 4 已经开始了会议,锁还没释放,在等待锁释放
。。。。
当线程4 释放锁时,离开会议,线程3 获取到锁,开始会议
注意,这里多线程并不是说谁先申请,谁先能得到会议室
Enter 会阻塞线程
20个线程有点多,这里改成5个线程
Monitor.Wait和Monitor.Pause()
Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
Pulse方法:唤醒一个等待队列中的线程。当这个线程被唤醒之后获取锁,将继续执行后续步骤。
Wait 和 Pulse 必须要写在获取锁之后
3 Mutex类
Mutex
using var mutex = new Mutex(false, "Global\\MyAppMutex");if (mutex.WaitOne(1000)) // 等待最多1秒
{try{// 临界区代码}finally{mutex.ReleaseMutex();}
}
3.1 特点
- 所有权: Mutex 具有线程的所有权,只有获取它的线程才能释放(ReleaseMutex),其它线程释放会抛出ApplicationException.
- 等待机制:WaitOne(timeout)方法等待获取锁。超时返回false,避免死锁
- 跨进程支持:命名Mutex(指定名称参数)可用于不同进程间的同步,是系统级资源。
注意事项:
必须释放:必须在Finally块中调用ReleaseMutex释放,否则会导致资源泄露和永久堵塞。
权限控制:跨进程Mutex可能需要设置访问权限,避免权限不足导致的异常
性能考量:Mutex是内核对象。性能比Lock Monitor 低,单进程内优先使用Lock.
3.3 使用
3.0 互斥体的创建
Mutex () 第一个 默认创建一个互斥体,调用的线程不具有互斥体的初始化所有权, 无互斥体名称,
Mutex(true)调用线程拥有互斥体初始化所有权, false :则没有
Mutex(true,“互斥体名称”) ,互斥体名称:Global\ 前缀(全局命名空间)被所有应用程序共享互斥体,可以与系统上的任何进程共享同步对象,Local\ 前缀(本地命名空间)用一个用户登录的终端共享。
由于它们是系统范围的,因此命名互斥体可用于协调跨进程边界的资源使用。
比如创建了一个本地同步互斥体:
using System.Diagnostics;
using static System.Net.Mime.MediaTypeNames;namespace Study02_命名互斥体
{internal class Program{public static void Main(){bool flag;Mutex m = new Mutex(false, "MyMutex", out flag);if(flag){Console.WriteLine("调用线程已赋予互斥体的初始所有权");}else{Console.WriteLine("互斥体的初始所有权由其它线程掌握");Process currentProcess = Process.GetCurrentProcess();foreach (Process process in Process.GetProcessesByName(currentProcess.ProcessName)){bool flag8 = process.Id != currentProcess.Id;if (flag8){break;}}Console.WriteLine("互斥体已经存在,请关闭");Console.ReadLine();Environment.Exit(0);}// 获取互斥体所有权m.WaitOne();Console.WriteLine("This application owns the mutex. " + "Press ENTER to release the mutex and exit.");Console.ReadLine();m.ReleaseMutex();}}
}
启动两次程序时:
第一个启动的程序已获取互斥体的初始所有权
第二个启动的程序未获得 通常情况下直接关闭,这样防止多个应用程序开启
3.1 基本使用
创建一个互斥对象的实例
线程通过 方法 Waitone 获取互斥体实例的所有权
方法 ReleaseMutex 仅由获取互斥体的线程调用
当两个或多个线程需要同时访问共享资源时,系统需要同步机制来确保一次只有一个线程使用该资源。 Mutex 是一个同步基元,它仅向一个线程授予对共享资源的独占访问权限。 如果线程获取互斥体,则要获取该互斥的第二个线程将暂停,直到第一个线程释放互斥体。
类 Mutex 强制实施线程标识,因此只能由获取它的线程释放互斥体。 相比之下, Semaphore 类不强制实施线程标识。 互斥体也可以跨应用程序域边界传递。
拥有互斥体的线程可以在重复调用 WaitOne 中请求相同的互斥,而不会阻止其执行。 但是,线程必须调用 ReleaseMutex 方法的次数相同,才能释放互斥体的所有权。
示例:
主线程已经释放了互斥体的所有权,此时互斥体的所有权已经被其它线程拥有,此时主线程再释放互斥体的所有权,就会出现异常:
附错误代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Study01
{public class using_Mutex{private static Mutex _mut;private const int _threadNum = 3;private const int _usingRes = 1;public static void TestMutex(){_mut=new Mutex(false);for (int i = 0; i < _threadNum; i++){Thread thread = new Thread(new ThreadStart(DoWork));thread.Name = "Thread" + i;thread.Start();}_mut.WaitOne();Console.WriteLine("调用的线程拥有互斥体的初始所有权");Thread.Sleep(200);Console.WriteLine("调用的线程释放互斥体");_mut.ReleaseMutex(); _mut.ReleaseMutex();}private static void DoWork(){for (int i = 0; i < _usingRes; i++){UsingResource();}}private static void UsingResource(){Console.WriteLine($"{Thread.CurrentThread.Name}请求使用临界区资源");if (_mut.WaitOne(1000)){Console.WriteLine($"{Thread.CurrentThread.Name}进入零界区资源");Thread.Sleep(800); // 模拟对资源的使用Console.WriteLine($"{Thread.CurrentThread.Name}离开零界区资源");_mut.ReleaseMutex();}else{Console.WriteLine($"{Thread.CurrentThread.Name}等待超时,未能进入零界区资源");}}}
}
2 读写锁(Reader-Writer Locks)
2.1 ReaderWriterLock
ReaderWriterLock
2.1.1 使用
构造函数: 只有一个无参构造函数
属性:
常用方法:
void AcquireReaderLock(int millisecondsTimeout) | 使用一个 Int32 超时值获取读线程锁。 |
---|---|
void ReleaseReaderLock () | 释放读线程锁 |
void AcquireWriterLock(int millisecondsTimeout); | 使用一个 Int32 超时值获取写线程锁。 |
void ReleaseWriterLock () | 减少写线程上的锁计数 |
System.Threading.LockCookie ReleaseLock (); | 释放锁,不管线程获取锁的次数如何 |
void RestoreLock(ref System.Threading.LockCookie lockCookie); | 将线程的锁状态还原为调用 ReleaseLock() 前的状态。 |
void DowngradeFromWriterLock (ref System.Threading.LockCookie lockCookie); | 将线程的锁状态还原为调用 UpgradeToWriterLock(Int32) 前的状态。 |
System.Threading.LockCookie UpgradeToWriterLock(int millisecondsTimeout); | 使用一个 Int32 超时值将读线程锁升级为写线程锁。 |
基本示例
学到这儿本人会产生一个疑问,为啥已经有读锁和写锁了,为啥还会有方法UpgradeToWriterLock,将读锁升级为写锁,DowngradeFromWriterLock 将写锁降级为读锁。
解惑
在“读多写少的场景中,有时线程会先读取数据(持有读锁),然后根据读取的结果决定是否修改数据(需要写锁)此时:
如果先释放读锁再申请写锁,中间有可能其它线程修改数据,导致数据 “读取-修改“的原子性被打破。
而 UpgradeToWriterLock 可以原子性地从读锁升级为写锁(无需先释放读锁),确保 读取- 修改的过程不被其它线程干扰。
UpgradeToWriterLock:从读锁升级为写锁
作用:当前线程已持有读锁时,原子地升级为写锁(期间阻塞其他所有线程,包括读线程)。
返回值:LockCookie 结构体,用于后续降级或释放锁
DowngradeFromWriterLock:从写锁降级为读锁
作用:将当前持有的写锁降级为读锁,允许其他读线程进入,但仍阻止其他写线程
将读锁升级为写锁
Thread thread1 = new Thread(() =>{rwl.AcquireReaderLock(Timeout.Infinite);try{Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} wrote value {resource}");// 将读锁升级为写锁LockCookie lockCookie1 = rwl.UpgradeToWriterLock(Timeout.Infinite);try{resource = 100;Console.WriteLine($"修改数据为{resource}");}finally{// 将写锁降级为读锁rwl.DowngradeFromWriterLock(ref lockCookie1);}}finally{rwl.ReleaseWriterLock();}});
2.1.2 示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static Study01.Using_ReaderWriterLock1.ReadWrite;namespace Study01
{public class Using_ReaderWriterLock1{public static void BaseStudyReadWriteLock(){RWApp.StartReadAndWrite();}public class ReadWrite{private ReaderWriterLock rwl=new ReaderWriterLock();private int a;private int b;public void ReadDatas(ref int a, ref int b){rwl.AcquireReaderLock(Timeout.Infinite);try{a = this.a;b = this.b;Console.WriteLine("read a =" + this.a + ", b =" + this.b + ",ThreadID=" + Thread.CurrentThread.GetHashCode());}finally{rwl.ReleaseReaderLock();}}public void writeDatas(int a, int b){rwl.AcquireWriterLock(Timeout.Infinite);this.a = a;this.b = b;try{Console.WriteLine("Begin write...");Console.WriteLine("Write a =" + this.a + ", b =" + this.b + ",ThreadID=" + Thread.CurrentThread.GetHashCode());}finally{rwl.ReleaseWriterLock();Console.WriteLine("End write!");}}public class RWApp{private ReadWrite rw = new ReadWrite();public static void StartReadAndWrite(){RWApp e = new RWApp();for (int i = 0; i < 2; i++){Thread thread = new Thread(new ThreadStart(e.Write));thread.Name = "WriteThread" + i;thread.Start();}for (int i = 0; i < 3; i++){Thread thread = new Thread(new ThreadStart(e.Read));thread.Name = "ReadThread" + i;thread.Start();}}private void Write(){int a = 666;int b = 888;for (int i = 0; i < 5; i++){this.rw.writeDatas(a++, b++);Thread.Sleep(1000);}}private void Read(){int a = 0;int b = 0;for (int i = 0; i < 5; i++){this.rw.ReadDatas(ref a, ref b);Thread.Sleep(1000);}}}}}
}
升级锁会造成死锁风险
若多个线程同时持有读锁并尝试升级为写锁,会导致死锁(每个线程都在等待其他线程释放读锁)。因此,ReaderWriterLock 的升级功能需谨慎使用。
2.2 ReaderWriterLockSlim
ReaderWriterLockSlim
ReaderWriterLockSlim 类要比ReaderWriterLock 方法更安全。
允许多个读操作并行,但写操作互斥:
性能优于旧的 ReaderWriterLock
定义支持单个写线程和多个读线程的锁。
2.2.1 成员变量和方法
构造函数
- ReaderWriterLockSlim() :使用默认属性值初始化 ReaderWriterLockSlim 类的新实例
- ReaderWriterLockSlim(LockRecursionPolicy)
在指定锁定递归策略的情况下初始化 ReaderWriterLockSlim 类的新实例。
recursionPolicy
LockRecursionPolicy
枚举值之一,用于指定锁定递归策略。
属性
方法
void EnterReadLock() | 尝试进入读取模式锁定状态 |
---|---|
void ExitReadLock() | 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。 |
void EnterWriteLock() | 尝试进入写入模式锁定状态 |
void ExitWriteLock() | 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。 |
void EnterUpgradeableReadLock() | 尝试进入可升级模式锁定状态 |
void ExitUpgradeableReadLock() | 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式。 |
bool TryEnterReadLock(int millisecondsTimeout) | 尝试进入读取模式锁定状态,可以选择超时时间。返回 如果调用线程已进入读取模式,则为 true;否则为 false。 |
bool TryEnterWriteLock(int millisecondsTimeout); | 尝试进入写入模式锁定状态,可以选择超时时间。返回 如果调用线程已进入写入模式,则为 true;否则为 false。 |
bool TryEnterUpgradeableReadLock(int millisecondsTimeout); | 尝试进入可升级模式锁定状态,可以选择超时时间。返回 如果调用线程已进入可升级模式,则为 true;否则为 false。 |
2.2.2 使用示例
升级锁使用示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Study01
{/// <summary>/// ReaderWriterLockSlim/// </summary>public class Using_ReaderWriterLockSlim{private static ReaderWriterLockSlim _rwls = new ReaderWriterLockSlim();public void BasePracticeReaderWriterLockSlim(){// 这些属性可以帮助你了解锁的当前状态int currentReadCount = _rwls.CurrentReadCount;bool isReadLockHeld = _rwls.IsReadLockHeld;bool isUpgradeableReadLockHeld = _rwls.IsUpgradeableReadLockHeld;bool isWriteLockHeld = _rwls.IsWriteLockHeld;int waitingReadCount = _rwls.WaitingReadCount;int waitingUpgradeCount = _rwls.WaitingUpgradeCount;int waitingWriteCount = _rwls.WaitingWriteCount;}public static Dictionary<string, string> _dictionary = new Dictionary<string, string>();public static AddOrUpdateStatus addOrUpdateStatus { get; set; }public static void ExampleReaderWriterLockSlim(){for (int i = 0; i < 3; i++){Thread thread = new Thread(WriteDictionary);thread.Name = $"{i}";thread.Start();}Thread.Sleep(100);Thread thread1 = new Thread(ReadDictionary);thread1.Name = $"3";thread1.Start();Thread thread2 = new Thread(ReadDictionary);thread2.Name = $"4";thread2.Start();}public static void ReadDictionary(){_rwls.EnterReadLock();try{Console.WriteLine($"线程{Thread.CurrentThread.Name}进入读取锁定状态***********");foreach (var item in _dictionary){// 读取字典中的数据}}finally{Console.WriteLine($"线程{Thread.CurrentThread.Name}离开读取锁定状态***********");_rwls.ExitReadLock();}}public static void WriteDictionary(){for (int i = 0; i < 100; i++){AddOrUpdateStatus addOrUpdateStatus1 = AddItems($"key{i}", i.ToString());}}/// <summary>/// 添加数据/// </summary>public static AddOrUpdateStatus AddItems(string key, string value){try{_rwls.EnterUpgradeableReadLock();Console.WriteLine($"线程{Thread.CurrentThread.Name}进入可升级模式的锁定状态");if (_dictionary.TryGetValue(key, out string str)){if (str.Equals(value)){return AddOrUpdateStatus.Unchanged;}else{try{_rwls.EnterWriteLock();Console.WriteLine($"线程{Thread.CurrentThread.Name}进入写入锁定状态 key={key}, Update value={value}");_dictionary[key] = value;return AddOrUpdateStatus.Updated;}finally{Console.WriteLine($"线程{Thread.CurrentThread.Name}离开写入锁定状态");_rwls.ExitWriteLock();}}}else{try{_rwls.EnterWriteLock();Console.WriteLine($"线程{Thread.CurrentThread.Name}进入写入锁定状态 add key={key}, value={value}");_dictionary.Add(key, value);return AddOrUpdateStatus.Added;}finally{Console.WriteLine($"线程{Thread.CurrentThread.Name}离开写入锁定状态");_rwls.ExitWriteLock();}}}finally{Console.WriteLine($"线程{Thread.CurrentThread.Name}离开可升级模式的锁定状态");_rwls.ExitUpgradeableReadLock();}}}public enum AddOrUpdateStatus{Added,Updated,Unchanged};
}
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();void ReadOperation()
{_rwLock.EnterReadLock();try{// 只读操作}finally{_rwLock.ExitReadLock();}
}void WriteOperation()
{_rwLock.EnterWriteLock();try{// 写操作}finally{_rwLock.ExitWriteLock();}
}
3 信号量(Semaphores)
3.1 Semaphore
控制同时访问资源的线程数量,支持跨进程
3.1.1 成员
构造函数
private static Semaphore _pool = new Semaphore(0, 3); // 初始0,最大3void AccessResource()
{_pool.WaitOne(); // 等待许可try{// 访问资源}finally{_pool.Release(); // 释放许可}
}
3.2 SemaphoreSlim
轻量级,支持异步
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3); // 初始3个许可async Task AccessResourceAsync()
{await _semaphore.WaitAsync(); // 异步等待try{// 访问资源}finally{_semaphore.Release();}
}
4. 事件(Auto /Manual ResetEvent)
4.1 AutoResetEveent
表示一个线程同步事件,当发出一个信号时,释放一个等待的线程,并自动重置,无法继承此类。
4.1.1 使用
构造
AutoResetEvent(Boolean)
使用布尔值初始化 AutoResetEvent 类的新实例,该值指示是否将初始状态设置为信号。
方法
bool Reset() | 将事件状态设置为非终止状态,导致线程阻止。 |
---|---|
bool WaitOne () | 阻止当前线程,直到当前 WaitHandle 收到信号。返回 如果当前实例收到信号,则为 true。 如果当前实例永不发出信号,则 WaitOne() 永不返回。 |
bool WaitOne (TimeSpan timeout) | 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。返回 如果当前实例收到信号,则为 true;否则为 false。 |
bool WaitOne (TimeSpan timeout, bool exitContext) | 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。返回 如果当前实例收到信号,则为 true;否则为 false。 |
4.1.2 示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Study01
{internal class Using_AutoResetEvent{public static AutoResetEvent autoResetEvent = new AutoResetEvent(false);public static void ExampleAutoResetEvent(){for (int i = 0; i < 3; i++){Thread thread = new Thread(DoWork1);thread.Name = $"Thread{i + 1}";thread.Start();}while (Console.ReadLine() == "yes"){DoWork2();}}private static void DoWork1(){if (autoResetEvent.WaitOne(1000)){Console.WriteLine($"{Thread.CurrentThread.Name} 开始工作...");// 模拟工作Thread.Sleep(2000);Console.WriteLine($"{Thread.CurrentThread.Name} 工作完成");}else{Console.WriteLine($"{Thread.CurrentThread.Name} 等待超时");}}private static void DoWork2(){Console.WriteLine($"{Thread.CurrentThread.Name} 发出信号");autoResetEvent.Set(); // 等待信号}}
}
4.2 ManualResetEvent
表示必须手动重置信号的线程同步事件。 无法继承此类。
4.2.1 使用
构造函数
ManualResetEvent(Boolean)
使用布尔值初始化 ManualResetEvent 类的新实例,该值指示是否将初始状态设置为信号。
方法
同上 AutoResetEvent
4.2.2 示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Study01
{internal class Using_ManualResetEvent{public static ManualResetEvent manualResetEvent = new ManualResetEvent(false);public static int Product = 10; // 模拟工作时间public const int MinProduct = 10;public static void ExampleManualResetEvent(){for (int i = 0; i < 3; i++){Thread thread = new Thread(ConsumeProduct){Name = $"消费线程{i + 1}"};thread.Start();}Console.WriteLine("是否生产产品 yes/no:\r\n");while (Console.ReadLine() == "yes"){MakeProduct();}}/// <summary>/// 制造产品/// </summary>public static void MakeProduct(){Random random = new Random();int nums = random.Next(0, 100);Interlocked.Exchange(ref Product, nums);Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine($"{Thread.CurrentThread.Name}正在生产产品{nums}");Thread.Sleep(1000); // 模拟生产时间if (nums > MinProduct){Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine($"{Thread.CurrentThread.Name}已生产足够的产品,发出信号");manualResetEvent.Set();}}/// <summary>/// 消费产品/// </summary>public static void ConsumeProduct(){while (true){manualResetEvent.WaitOne();if (Product > MinProduct){Console.ForegroundColor = ConsoleColor.Green;int num = Interlocked.Decrement(ref Product);Console.WriteLine($"{Thread.CurrentThread.Name}正在消费产品{num + 1}");Random random = new Random(100);int consumTime = random.Next(100, 300);Thread.Sleep(consumTime);}else{Console.ForegroundColor = ConsoleColor.Yellow;Console.WriteLine($"{Thread.CurrentThread.Name}产品不足,等待生产");manualResetEvent.Reset(); // 重置事件状态Console.WriteLine("是否生产产品 yes/no:\r\n");}}}}
}
4.3 ManualResetEventSlim
表示线程同步事件,收到信号时,必须手动重置该事件。 此类是 ManualResetEvent 的轻量替代项。
4.3.1 使用
构造函数
属性
方法
void Set () | 将事件状态设置为有信号,从而允许一个或多个等待该事件的线程继续 |
---|---|
void Reset() | 将事件状态设置为非终止,从而导致线程受阻。 |
void Wait () | 阻止当前线程,直到设置了当前 ManualResetEventSlim 为止。 |
bool Wait (int millisecondsTimeout) | 阻止当前线程,直到设置了当前 ManualResetEventSlim 为止,同时使用 32 位带符号整数测量时间间隔,返回 如果已设置 ManualResetEventSlim,则为 true;否则为 false。 |
void Wait (System.Threading.CancellationToken cancellationToken); | 阻止当前线程,直到当前 ManualResetEventSlim 收到信号为止,同时观察 CancellationToken。 |
bool Wait (int millisecondsTimeout, System.Threading.CancellationToken cancellationToken) | 阻止当前线程,直到设置了当前 ManualResetEventSlim 为止,并使用 32 位带符号整数测量时间间隔,同时观察 CancellationToken。 |
4.3.2 示例
/// <summary>
/// 超时和取消示例
/// </summary>
public static void Using_CancellationTokenSource()
{ManualResetEventSlim manualResetEventSlim = new ManualResetEventSlim(false);CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();Task.Run(() =>{try{Console.WriteLine($"启动一个长时间开始的任务");Thread.Sleep(5000);}catch (OperationCanceledException){Console.WriteLine("操作被取消");}});Task.Run(() =>{try{// 有信号singal为true 等待超时为false 同 CancellationSource 取消 直接抛出异常OperationCancelledException bool singal = manualResetEventSlim.Wait(2000, cancellationTokenSource.Token);string task = singal ? "任务被设置" : "操作被取消";Console.WriteLine($"任务等待结束{task}");}catch (OperationCanceledException){Console.WriteLine("操作被取消");}});Thread.Sleep(1000);// manualResetEventSlim.Set();cancellationTokenSource.Cancel();
}
5.原子操作 (Interlocked)
5.1 Interlocked 类
为多个线程共享的变量提供原子操作。
多个线程使用同一个变量进行操作时,并不知道此变量已经在其它线程中发生改变,导致执行完毕后结果不符合期望。
例如:
图文来自博客:
痴者工良
5.1.1 使用
特点:静态类
常用方法
int Add(ref int location1, int value) | 添加两个 整数,并将第一个整数替换为总和,作为原子操作 |
---|---|
long CompareExchange(ref long location1, long value, long comparand); | location 的 值与 Comparand的值比较,相等则 Location 的值 为value, 否则,Location 为原始值,返回location 的原始值 |
int Increment(ref int location) | 以原子操作的形式递增指定变量的值并存储结果。,返回递增之后的值 |
long Decrement(ref long location); | 以原子操作的形式递减指定变量的值并存储结果。 |
float Exchange (ref float location1, float value) | 将单精度浮点数设置为指定值,并将原始值作为原子操作返回 |
方法解读:
int value = 0,a,b,c;// usingResource 初始值为0// usingResource = 0+ 1(value);// 返回:usingResource 值value = Interlocked.Add(ref usingResource, 1);Console.WriteLine(value);Console.WriteLine(usingResource);// usingResource 初始值为1// usingResource = 1+ 4(value);// 返回:usingResource 值为5value = Interlocked.Add(ref usingResource, 4);Console.WriteLine(value);Console.WriteLine(usingResource);// usingResource 初始值为5// usingResource = 5== 5(comparand) ?: comparand: 2(value);// 返回:usingResource 值为5a = Interlocked.CompareExchange(ref usingResource, 2, 5);Console.WriteLine(a);Console.WriteLine(usingResource);// usingResource 初始值为5// usingResource = 5== 8(comparand) ?: comparand: 2(value);// 返回:usingResource 原始值为 2a = Interlocked.CompareExchange(ref usingResource, 2, 8);Console.WriteLine(a);Console.WriteLine(usingResource);// usingResource 初始值为2// usingResource = 2+ 1(value);// 返回:usingResource 值为2b = Interlocked.Increment(ref usingResource);Console.WriteLine(b);Console.WriteLine(usingResource);// usingResource 初始值为3// usingResource = 10(value);// 返回:usingResource 原始值为 3c = Interlocked.Exchange(ref usingResource, 10);Console.WriteLine(c);Console.WriteLine(usingResource);
5.1.2 示例
private int _counter = 0;void Increment()
{Interlocked.Increment(ref _counter);
}void UpdateValue()
{int original, newValue;do{original = _counter;newValue = CalculateNewValue(original);} while (Interlocked.CompareExchange(ref _counter, newValue, original) != original);
}
6.自旋锁(SpinLock)
自旋锁
避免上下文切换,适合极短临界区
适用于低争用、短时间持有的场景
定义:
提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。
构造函数:
SpinLock (bool enableThreadOwnerTracking)
使用用于跟踪线程 ID 以改善调试的选项初始化 SpinLock 结构的新实例。
属性
方法
void Enter (ref bool lockTaken) | 采用可靠的方式获取锁,如果已获取锁,则为 true,否则为 false。 调用此方法前,必须将 lockTaken 始化为 false。 |
---|---|
void Exit () | 释放锁 |
void Exit (bool useMemoryBarrier) | 一个布尔值,该值指示是否应发出内存界定,以便将退出操作立即发布到其他线程。 |
void TryEnter(ref bool lockTaken) | 尝试采用可靠的方式获取锁,如果已获取锁,则为 true,否则为 false。 调用此方法前,必须将 lockTaken 始化为 false。与 Enter不同,TryEnter 不会阻止等待锁可用。 如果在调用 TryEnter 时锁不可用,它将立即返回,而不会进一步旋转。 |
void TryEnter(int millisecondsTimeout, ref bool lockTaken) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。millisecondsTimeout Int32:等待的毫秒数,或为 Infinite (-1),表示无限期等待。lockTaken Boolean:如果已获取锁,则为 true,否则为 false。 调用此方法前,必须将 lockTaken 始化为 false。 |
6.1 使用
注释:
SpinLock 是一种低级互斥锁,它在尝试获取锁时会在循环中“自旋”,而不是立即阻塞线程。
它适用于锁持有时间非常短的场景,因为自旋可以减少线程上下文切换的开销,但如果锁持有时间较长,则会浪费CPU周期。
注意:SpinLock 是结构体,因此必须通过引用传递(如果要在多个方法中使用或作为类成员,通常使用ref)。
另外,SpinLock 是线程敏感的,因此必须确保不要复制它。
重要:由于SpinLock是值类型,如果你在类中作为字段,应该使用ref关键字来确保不会复制。
使用SpinLock时需要注意:
- 避免在持有SpinLock时调用可能阻塞的方法(如IO操作、等待另一个同步原语等)。
- 避免递归调用,默认情况下SpinLock不支持递归(除非在构造函数中启用递归)。
- 使用Enter(ref bool lockTaken)模式,确保在异常情况下也能正确释放锁。
6.2 示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace Study01
{internal class Using_SpinLock{public static SpinLock spinLock = new SpinLock();public static int Resource = 0;public static void BaseUsageExample(){Console.WriteLine("SpinLock的基本使用");bool lockTaken = false;Task.Factory.StartNew(() =>{spinLock.Enter(ref lockTaken);try{Console.WriteLine("线程1获取锁成功");Resource++;Console.WriteLine($"线程1修改资源值为: {Resource}");}finally{if (lockTaken){spinLock.Exit();Console.WriteLine("线程1释放锁");}}});}public static void MultiThreadExample(){//说明锁并没有阻塞线程Console.WriteLine("多线程竞争示例");Resource = 0;List<Task> tasks = new List<Task>();for (int i = 0; i < 10; i++){tasks.Add(Task.Factory.StartNew(() =>{bool lockTaken = false;spinLock.Enter(ref lockTaken);try{Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}获取锁成功");Resource++;Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}修改资源值为: {Resource}");}finally{if (lockTaken){spinLock.Exit();Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}释放锁");}}}));}Task.WaitAll(tasks.ToArray());Console.WriteLine($"最终共享资源值: {Resource}");}public static void TimeoutAndExceptionExample(){// 此示例说明:TryEnter 不阻塞Console.WriteLine("超时和异常处理示例");for (int i = 0; i < 5; i++){Task task1 = Task.Run(() =>{bool lockToken = false;try{spinLock.TryEnter(200, ref lockToken);if (lockToken){Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}获取到锁");Thread.Sleep(500);throw new ApplicationException("测试异常");}else{Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}超时未获取到锁");}}catch (Exception ex){Console.WriteLine($"发生异常{ex.Message}");}finally{if (lockToken){spinLock.Exit();Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}释放锁");}}});Thread.Sleep(100);}}public static Dictionary<string, string> Keys = new Dictionary<string, string>();public static void AdvancedUsageExample(){Console.WriteLine("SpinLock的高级用法");for (int j = 0; j < 10; j++){Task task = Task.Run(() =>{for (int i = 0; i < 100; i++){string key = $"Key{i}";string value = $"Value{i}";Add(key, value);}});}for (int j = 0; j < 10; j++){Task task = Task.Run(() =>{for (int i = 80; i >0; i--){string key = $"Key{i}";string value = $"Value{i}";GetValue(key);}});}}public static void Add(string key, string value){bool lockToken = false;try{spinLock.Enter(ref lockToken);if (lockToken){if (!Keys.ContainsKey(key)){Keys.Add(key, value);Console.ForegroundColor = ConsoleColor.Green;Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}添加键值对: {key} - {value}");}else{Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}发现键已存在: {key}");}}}finally{if (lockToken){Console.ForegroundColor = ConsoleColor.DarkCyan;Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}释放锁");spinLock.Exit();}}}public static string GetValue(string key){bool lockToken = false;try{spinLock.Enter(ref lockToken);Console.ForegroundColor = ConsoleColor.Yellow;if (Keys.ContainsKey(key)){Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}获取键{key}的值{Keys[key]}");return Keys[key];}else{Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}发现键不存在: {key}");return null;}}finally{if (lockToken){Console.ForegroundColor = ConsoleColor.DarkCyan;Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}释放锁");spinLock.Exit();}}}}
}
6.3 场景
使用场景:
- 锁持有时间非常短(微秒级别)
- 竞争不激烈的情况
- 不能使用阻塞锁的高性能场景
重要特性:
- 是值类型(结构体),需要小心使用避免复制
- 不支持递归调用(同一线程重复获取会导致死锁)
- 使用 ref bool lockTaken 模式确保异常安全
与其它锁的比较:
- 比 lock 关键字(Monitor)更轻量
- 比 Mutex 性能更高
- 在等待时间较长时性能不如阻塞锁
最佳实践:
- 始终使用 try-finally 确保锁被释放
- 避免在持有 SpinLock 时执行耗时操作
- 考虑使用 TryEnter 带超时版本避免无限期等待
7.Volatile 关键字
7.1 了解
在没有适当内存屏障的情况下,一个线程对某个字段的修改可能不会立即被其他线程看到。这是因为:
- 编译器优化:编译器可能会将字段的值缓存到CPU寄存器中,后续的读取都直接使用这个寄存器中的副本,而不是去读内存中的最新值
- CPU缓存:现代CPU有多级缓存,一个线程在CPU核心1上修改了数据,可能还留在核心1的缓存里,没有立即刷回到主内存,导致在CPU核心2上运行的线程看不到这个修改
volatile 通过插入一个内存屏障来解决这个问题。对 volatile 字段的写操作会确保其结果立即对其他所有线程可见。
7.2 使用
类型
volatile 只能用于类或结构体的字段,不能用于局部变量或参数。它可以用于以下类型的字段:
- 引用类型
简单数值类型(如 sbyte, byte, short, ushort, int, uint, char, float, bool) - 枚举类型(其基础类型为上述类型)
- IntPtr 和 UIntPtr
语法:
csharp
private volatile int _counter;
private volatile bool _isDone;
注意事项
-
不保证原子性:,Volatile 不能替代锁,它只解决可见性和重排序问题,但不能保证复合操作的原子性。
错误示例: Volatile int x; x++;
读–》 改–》写 三个操作,不是原子的,即使x是 Volatile 的。多个线程同时执行 x++ 任然会导致更新丢失。
解决方案:对于原子性操作,应使用Interlocked类,(Interlocked.Increment)或lock. -
适用场景有限:Volatile 最适合简单的,独立的标志或状态指示器(如 bool) ,或者像双重检查锁定这样的特定模式。对于复杂的同步,应优先考虑lock,monitor,mutex 或 ReaderWriterLockSlim.
总结:
当有一个简单的字段 如bool , int,它会被多个线程读写,并且你希望一个线程的写入能立即被其它线程看到。同时防止指令重排破坏逻辑时。就使用Volatile。对于任何需要原子性的操作,都应该选择Lock 或 Interlocked.
internal class LongThread{public static readonly LongThread Instance = new Lazy<LongThread>(() => new LongThread()).Value;private object _lock = new object();private List<TaskInfo> _taskInfos = new List<TaskInfo>();public CancellationTokenSource token;private Task LongTask;private int _delayTime = 100;public int DelayTime { get => _delayTime; set => _delayTime = value; }private LongThread(){token = new CancellationTokenSource();LongTask = Task.Factory.StartNew(() => {Thread.Sleep(_delayTime);while (!token.IsCancellationRequested){try{lock (_lock){foreach (TaskInfo info in _taskInfos){info.PerformTask();}}}catch (Exception) { }Thread.Sleep(1);}}, TaskCreationOptions.LongRunning);}private class TaskInfo{public TimeSpan intervalTime;public Action actionTask;private DateTime _startTime = DateTime.Now;private volatile bool _FinishState = true;public TaskInfo(TimeSpan intervalTime, Action actionTask){this.intervalTime = intervalTime;this.actionTask = actionTask;}public void PerformTask(){if (_FinishState && (DateTime.Now - _startTime) > intervalTime){_FinishState = false;_startTime = DateTime.Now;Task.Run(() => {actionTask?.Invoke();}).ContinueWith(t => {_FinishState = true;});}}}public void AddAction(Action action, TimeSpan intervalTime){lock (_lock){if (!_taskInfos.Any(item => item.actionTask == action)){_taskInfos.Add(new TaskInfo(intervalTime, action));}}}public void RemoveAction(Action action){lock (_lock){TaskInfo taskItem = null;try{taskItem = _taskInfos.First(item => item.actionTask == action);if (taskItem != null) _taskInfos.Remove(taskItem);}catch (Exception) { }}}public void DestoryAll(){if (LongTask != null){token.Cancel();LongTask.Wait();LongTask = null;}}~LongThread(){if (LongTask != null){token.Cancel();LongTask.Wait();LongTask = null;}}}
- 确保不同线程对_FinishState的修改能被其他线程立即看到
- 防止编译器优化导致的线程间数据不一致问题
- 保证了该字段的读写操作具有内存可见性
2.按照作用范围分类
2.1 进程内锁
- lock / Monitor
- ReaderWriterLockSlim
- SpinLock
- SemaphoreSlim
- 只在同一应用程序域内有效
2.1 跨进程锁
- Mutex
- Semaphore
- 需要命名,系统范围可见
- SemaphoreSlim
- 常用于单实例应用程序
3.基于获取临界资源公平性分类
3.1 非公平锁
- Lock Monitor
- 新请求的线程可能比等待队列中的线程先获取锁
- 吞吐量更高
3.2 公平锁
- 请求线程按照顺序获取锁
- 在.net 中实现较为复杂
- 可通过Semaphore
4.基于阻塞行为的分类
4.1 阻塞锁
- 获取不到锁时线程休眠
- lock ,mutex, Semaphore.WaitOne()
- 适合可能长时间等待的场景
4.2 非阻塞锁
- 尝试获取锁,失败立即返回
- Monitor.TryEnter()
- SemaphoreSlim.Wait(0)
- 适合需要快速失败处理的场景
4.3 自旋锁
- SpinLock
- 在用户空间忙等待
- 适合极短临界区
5.特殊场景锁
5.1 异步兼容锁
- SemaphoreSlim.WaitAsync()
- AsyncLock (需自定义或使用第三方库)
public class AsyncLock
{private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);public async Task<IDisposable> LockAsync(){await _semaphore.WaitAsync();return new Releaser(_semaphore);}private struct Releaser : IDisposable{private SemaphoreSlim _semaphore;public Releaser(SemaphoreSlim semaphore) => _semaphore = semaphore;public void Dispose() => _semaphore?.Release();}
}
原文引用自博文
微软文档
https://segmentfault.com/a/1190000046460179
痴者工良