知识需要不断积累, 总结和沉淀, 思考和写作是成长的催化剂
梯子
一, 锁 1,lock2,Interlocked3,Monitor4,SpinLock5,Mutex6,Semaphore7,Events1,AutoResetEvent2,ManualResetEvent3,ManualResetEventSlim8,ReaderWriterLock 二, 线程安全集合三, 多线程模型 1, 同步编程模型 SPM2, 异步编程模型 APM3, 基于事件编程模型 EAP4, 基于任务编程模型 TAP 四, End
一, 锁
数据库中也有锁概念, 行锁, 表锁, 事物锁等, 锁的作用就是控制并发情况下数据的安全一致, 使一个数据被操作时, 其他并发线程等待. 开发方面多线程并行编程访问共享数据时, 为保证数据的一致安全, 有时需要使用锁来锁定对象来达到同步
.NET 中提供很多线程同步技术. 有 lock,Interlocked,Monitor 等用于进程内同步锁, Mutex 互斥锁, Semaphore 信号量, Events,ReaderWriterLockSlim 读写锁等用于多个进程间的线程同步
1,lock
lock 语句是设置对锁定和解除锁定的一种简单方式, 也是最常用的一种同步方式. lock 用于锁定一个引用类型字段, 当线程执行到 Lock 处, 会锁定该字段, 使之只有一个线程进入 lock 语句块内, 才 lock 语句结束位置再释放锁定, 另一个线程才可以进入. 原理运用同步块索引, 感兴趣可以研究下
- lock (obj)
- {
- //synchronized region
- }
因为只有一个线程可以进去, 没有并发, 所以牺牲了性能, 所以要尽量缩小 lock 的范围, 另一个建议是首选锁一个私有变量, 也就是 SyncRoot 模式, 声明一个 syncRoot 的私有 object 变量来进行锁定, 而不是使用 lock(this), 因为外面调用者也可能锁定你这个对象的实例, 但他并不知道你内部也使用了锁, 所以容易造成死锁
- private object syscRoot = new object();
- public void DoThis()
- {
- lock (syscRoot)
- {
- // 同一个时间只有一个线程能到达这里
- }
- }
- 2,Interlocked
InterLoacked 用于将变量的一些简单操作原子化, 也就是线程安全同步. 我们常写的 i++ 就不是线程安全的, 从内存中取值然后 + 1 然后放回内存中, 过程中很可能被其他线程打断, 比如在你 + 1 后放回内存时, 另一个线程已经先放回去了, 也就不同步了. InerLocked 类提供了以线程安全的方式递增, 递减, 交换, 读取值的方法
比如以下代替 lock 的递增方式
- int num = 0;
- //lock (syscRoot)
- //{
- // num++;
- //
- }
- num = Interlocked.Increment(ref num);
- 3,Monitor
上面 lock 就是 Monitor 的语法糖, 通过编译器编译会生成 Monitor 的代码, 像下面这样
- lock (syscRoot)
- {
- //synchronized region
- }
- // 上面的 lock 锁等同于下面 Monitor
- Monitor.Enter(syscRoot);
- try
- {
- //synchronized region
- }
- finally
- {
- Monitor.Exit(syscRoot);
- }
Monitor 不同于 Lock 就是它还可以设置超时时间, 不会无限制的等待下去.
- bool lockTaken = false;
- Monitor.TryEnter(syscRoot,500,ref lockTaken);
- if (lockTaken)
- {
- try
- {
- //synchronized region
- }
- finally
- {
- Monitor.Exit(syscRoot);
- }
- }
- else
- {
- }
- 4,SpinLock
SpinLock 自旋锁是一种用户模式锁. 对了, 插一嘴锁分为内核模式锁和用户模式锁, 内核模式就是在系统级别让线程中断, 收到信号时再切回来继续干活, 用户模式就是通过一些 CPU 指定或则死循环让线程一直运行着直到可用. 各有优缺点吧, 内核 CPU 资源利用率高, 但切换损耗, 用户模式就相反, 如果锁定时间较长, 就会白白循环等待, 后面就有混合模式锁的出现了
如果有大量的锁定, 且锁定时间非常短, SpinLock 就很有用, 用法和 Monitor 类似, Enter 或 TryEnter 获取锁, Exit 释放锁. IsHeld 和 IsHeldByCurrentThread 指定它当前是否锁定
另外 SpinLock 是个结构类型, 所以注意拷贝赋值时会创建全新副本问题. 必要时可按引用来传递
5,Mutex
Mutex 互斥锁提供跨多个进程同步一个类, 定义互斥锁的时候可以指定互斥锁的名称, 这样系统能够识别, 所以在另一个进程中定义的互斥, 其他进程也是可以访问到的, Mutex.OpenExisting()便可以得到.
- bool createdNew = false;
- Mutex mutex = new Mutex(false, "ProCharpMutex", out createdNew);
- if (mutex.WaitOne())
- {
- try
- {
- //synchronized region
- }
- finally
- {
- mutex.ReleaseMutex();
- }
- }
介于此我们可以用来禁止一个应用程序启动两次, 一般我们通过进程的名称来判断, 这里我们使用 Mutex 实现
- bool createdNew = false;
- Mutex mutex = new Mutex(false, "SingletonWinAppMutex", out createdNew);
- if (!createdNew)
- {
- MessageBox.Show("应用程序已经启动过了");
- Application.Exit();
- return;
- }
- 6,Semaphore
Semaphore 信号量和互斥类似, 区别是, 信号量可以同时让多个线程使用, 是一种计数的互斥锁定. 通过计数允许同时有几个线程访问受保护的资源. 也可以指定信号量名称以使在多个进程间共享
Semaphore 和上面 Mutex 都是继承自 WaitHandle 基类, WaitHandle 用于等待一个信号的设置, 嗲用 Wait, 线程会等待接收一个与等待句柄相关的信号
SemaphoreSlim 是对 Semaphore 的轻量替代版本 (它不继承 WaitHandle),SemaphoreSlim(int initialCount, int maxCount) 构造函数可指定最大并发个数, 然后在线程内通过 SemaphoreSlim 的 Wait 等到直到来接收信号是否可以进去受保护代码块了, 最后记得要 Release, 不然下一个线程获取不到准许进入的信号
7,Events
Events 事件锁不同于委托中的事件, 在 System.Threading 命名空间下, 用于系统范围内的事件资源的同步, 有 AutoResetEvent 自动事件锁, ManualResetEvent 手动事件锁以及轻量版本 ManualResetEventSlim
1,AutoResetEvent
AutoResetEvent 也是继承自 waitHandle 类的, 也是通过 WaitOne 来等待直到有信号, 它有两种状态: 终止和非终止, 可以调用 set 和 reset 方法使对象进入终止和非终止状态. 通俗点就是 set 有信号, 另一个线程可以进入了, reset 非终止无信息, 其他线程就阻塞了. 自动的意思就是一个线程进入了, 自动 Reset 设置无信号了其他线程就进不去了. 类似现实中的汽车收费口, 一杆一车模式
- private AutoResetEvent autoEvent = new AutoResetEvent(false);
- public void DoThis()
- {
- autoEvent.WaitOne();
- // 执行同步代码块
- autoEvent.Set();
- }
2,ManualResetEvent
手动事件锁和自动的区别在于, 手动事件锁没有信号时会阻塞一批线程的, 有信号时, 所有线程都运行, 同时唤醒多个线程, 除非手动 Reset 再阻塞, 类似现实场景中火车道路口的栅栏, 落杆拦截一批人, 起杆则一批人蜂拥通过, 用法和上面一样, WaitOne 等待信号, 结束时通过 Set 来通知有信号了, 可以通过了
3,ManualResetEventSlim
ManualResetEventSlim 通过封装 ManualResetEvent 提供了自旋等待和内核等待的混合锁模式. 如果需要跨进程或者跨 AppDomain 的同步, 那么就必须使用 ManualResetEvent.ManualResetEventSlim 使用 Wait 来阻塞线程, 支持任务的取消. 和 SemaphoreSlim 的 Wait 一样, 内部先通过用户模式自旋然后再通过内核模式效率更高.
8,ReaderWriterLock
ReaderWriterLock 读写锁不是从限定线程个数的角度来保护资源, 而是按读写角度来区分, 就是你可以锁定当某一类线程 (写线程) 中一个进入受保护资源时, 另一类线程 (读线程) 全部阻塞. 如果没有写入线程锁定资源, 就允许多个读取线程方法资源, 但只能有一个写入线程锁定该资源
具体用法参考示例
- // 创建读写锁
- ReaderWriterLock rwLock = new ReaderWriterLock();
- // 当前线程获取读锁, 参数为: 超时值(毫秒)
- rwLock.AcquireReaderLock(250);
- // 判断当前线程是否持有读锁
- if (!rwLock.IsReaderLockHeld)
- {
- return;
- }
- Console.WriteLine("拿到了读锁......");
- // 将读锁升级为写锁, 锁参数为: 超时值(毫秒)
- LockCookie cookie = rwLock.UpgradeToWriterLock(250);
- // 判断当前线程是否持有写锁
- if (rwLock.IsWriterLockHeld)
- {
- Console.WriteLine("升级到了写锁......");
- // 将锁还原到之前所的级别, 也就是读锁
- rwLock.DowngradeFromWriterLock(ref cookie);
- }
- // 释放读锁(减少锁计数, 直到计数达到零时, 锁被释放)
- rwLock.ReleaseReaderLock();
- Console.WriteLine("顺利执行完毕......");
- // 当前线程获取写锁, 参数为: 超时值(毫秒)
- rwLock.AcquireWriterLock(250);
- // 判断当前线程是否持有写锁
- if (rwLock.IsWriterLockHeld)
- {
- Console.WriteLine("拿到了写锁......");
- // 释放写锁(将减少写锁计数, 直到计数变为零, 释放锁)
- rwLock.ReleaseWriterLock();
- }
- // 释放写锁(将减少写锁计数, 直到计数变为零, 释放锁)
- // 当前线程不持有锁, 会抛出异常
- rwLock.ReleaseWriterLock();
- Console.WriteLine("顺利执行完毕......");
- Console.ReadLine();
ReaderWriterLockSlim 同样是 ReaderWriterLock 的轻量优化版本, 简化了递归, 升级和降级锁定状态的规则.
1. EnterWriteLock 进入写模式锁定状态
2. EnterReadLock 进入读模式锁定状态
3. EnterUpgradeableReadLock 进入可升级的读模式锁定状态
并且三种锁定模式都有超时机制, 对应 Try... 方法, 退出相应的模式则使用 Exit... 方法, 而且所有的方法都必须是成对出现的
二, 线程安全集合
并行环境下修改共享变量为了保证资源安全, 通常使用上面介绍的锁或信号量来解决此问题. 其实. NET 也内置了一些线程安全的集合, 使用他们就像使用单线程集合一样.
类型 | 描述 |
---|---|
BlockingCollection | 提供针对实现 IProducerConsumerCollection 的任何类型的限制和阻塞功能。 有关详细信息,请参阅 BlockingCollection 概述。 |
ConcurrentDictionary<tkey,tvalue style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;"> | 键 / 值对字典的线程安全实现。 |
ConcurrentQueue | FIFO(先进先出)队列的线程安全实现。 |
ConcurrentStack | LIFO(后进先出)堆栈的线程安全实现。 |
ConcurrentBag | 无序的元素集合的线程安全实现。 |
IProducerConsumerCollection | 类型必须实现以在 BlockingCollection 中使用的接口。 |
三, 多线程模型
1, 同步编程模型 SPM
2, 异步编程模型 APM
我们常见的 XXBegin, XXEnd 这两个经典的配对方法就是异步的, Begin 后会委托给线程池调用一个线程去执行. 还有委托的 BeginInvoke 调用
- FileStream fs = new FileStream("D:\\test.txt", FileMode.Open);
- var bytes = new byte[fs.Length];
- fs.BeginRead(bytes, 0, bytes.Length, (aysc) =>
- {
- var num = fs.EndRead(aysc);
- }, string.Empty);
3, 基于事件编程模型 EAP
WinFrom/WPF 开发中的 BackgroundWorker 类就是异步事件模式的一种实现方案, RunWorkerasync 方法启动与 DoWork 事件异步关联的方法, 工作完成后, 就触发 RunWorkerCompleted 事件, 也支持 CancelAysnc 方法取消以及 ReportProgress 通知进度等. 还又一个典型的就是 webClient
- WebClient client = new WebClient();
- client.DownloadDataCompleted += (sender,e)=>
- {
- };
- client.DownloadDataAsync(new Uri("https://www.baidu.com/"));
4, 基于任务编程模型 TAP
Task 出来后, 微软就大力推广基于 Task 的异步编程模型, APM 和 EAP 都被包装成 Task 使用. 下面示例简单用 Task 封装上面的编程模型. WebClient 的 DownloadDataTaskAsync 实现和示例中的类似, 利用一个 TaskCompletionSource 包装器包装成 Task
- FileStream fs = new FileStream("D:\\test.txt", FileMode.Open);
- var bytes = new byte[fs.Length];
- var task = Task.Factory.FromAsync(fs.BeginRead, fs.EndRead, bytes, 0, bytes.Length, string.Empty);
- var nums = task.Result;
- Action action = () =>{ };
- var task = Task.Factory.FromAsync(action.BeginInvoke, action.EndInvoke, string.Empty);
- public static Task<int> GetTaskAsuc(string url)
- {
- TaskCompletionSource<int> source = new TaskCompletionSource<int>();// 包装器
- WebClient client = new WebClient();
- client.DownloadDataCompleted += (sender, e) =>
- {
- try
- {
- source.TrySetResult(e.Result.Length);
- }
- catch (Exception ex)
- {
- source.TrySetException(ex);
- }
- };
- client.DownloadDataAsync(new Uri(url));
- return source.Task;
- }
四, End
最近几篇介绍了如何编写多线程和多任务应用程序. 在应用程序开发过程中要仔细规划, 太多的线程导致资源问题, 太少则起不到大效果. 多线程编程中一个中肯的建议就是
尽量避免修改共享变量, 使同步的要求变低. 通过合理规划可以减少大部分的同步复杂度.
- Search the fucking Web
- Read the fucking maunal
- --GoodGoodStudy
来源: https://www.cnblogs.com/xibei/p/12001805.html