- 在一个应用程序中使用多线程
- 好处是每一个线程异步地执行.
- 对于Winform程序,可以在后台执行耗时操作的同时,保持前台UI正常地响应用户操作.
- 对于Service.对于客户端的每一个请求,可以使用一个单独的线程来进行处理.而不是等到前一个用户的请求被完全处理完毕后,才能接着处理下一个用户的请求.
- 同时,异步带来的问题是,必须协调对资源(文件,网络,磁盘)的访问.
- 否则,会造成在同一时间两个以上的线程访问同一资源,并且这些线程间相互未知,导致不可预测的数据问题.
- 好处是每一个线程异步地执行.
- Lock/Monitor:防止线程敏感的代码块被并行执行.
- Lock/SyncLock语句块
- 保证一个代码块完整的执行期间,不会受到其他线程的中断影响.直到执行完成.
- 方式:在代码块的存续期间内,获得一个给定对象的互斥锁定.
- 参数必须是引用类型.
- 用来定义锁定的范围.
- 参数对象是用来唯一确定要在多个线程间共享的资源,所以,可以是任何的对象实例.
- 但是,一般使用多线程需要同步的资源对象.
- 传递值类型会进行装箱.
- 最佳实践:尽量避免lock共有类型,或者超出被同步代码控制的对象实例.
- Lock(this).其它代码块可能也会Lock该公用实例,容易导致多个线程互相等待相同对象的锁定的释放,从而造成死锁.
- Lock(Typeof(publicType)).锁定共有类型,也会造成相同的问题.
- Lock("myStr").字符串驻留在CLR中,整个应用程序中,对于同一字符串,只有一个实例.所以,锁住一个字符串,会导致在所有的线程中,导致对该字符串的锁定.
- 所以,应该Lock私有/受保护的成员.
- 一些Class还提供了一些专门用以锁定的成员.Array类和其它很多的集合类型都提供了SyncRoot.
- Monitor
- 功能上与Lock语句是等效的
- 锁定范围不应该跨越多个方法.
-
1 lock (x) 2 { 3 DoSomething(); 4 } 5 6 //the same. 7 8 try 9 { 10 DoSomething(); 11 } 12 finally 13 { 14 System.Threading.Monitor.Exit(obj); 15 }
- 一般使用Lock语句,因为其已经隐式包含了finally.
- 当同步对象实现了MarshalByRefObject时,可以穿过APP Domain的边界.
- 一个同步对象持有的引用
- 当前持有Lock的线程.
- 一个Ready队列(准备好可以获取获取Lock的线程).
- 一个Waiting队列(等待对象的状态变化通知).
- 方法
- TryEnter.相比于Enter方法的一直等待,可以传递Timeout,然后当等待指定时间后,返回false.
- Wait.使线程进入同步对象的Waiting队列中.指定的超时时间过后,进入Ready队列.
- Plause/PlauseAll.当线程将要释放Lock或者调用Wait方法时,调用该方法能够将1/N个Waiting队列中的线程进入Ready队列.
- Wait/Plause方法必须在同步块内调用.
- 功能上与Lock语句是等效的
- SpinLock
- .Net4.0以后提供.当Monitor的使用造成了性能问题时考虑使用.
- 内部使用一个无限循环来判断资源是否可用.
- 当等待时间过长时,会消耗更多的CPU时间.
- 在细粒度Lock,且Lock数量庞大,且基本上Lock时间很短时适用.
- 当持有SpinLock时,应尽量避免以下的动作
- Blocking.
- 调用其它可能Block的事物.
- 同时持有多个SpinLock.
- 动态调用(接口,虚方法).
- 调用不属于自己的代码.
- 分配内存.
- 本身是值类型(Structure).
- 如果必须要被传递时,适用ref传递.
- 不要使用只读字段存储它.
- 同步化事件或者等待句柄
- Lock/Montior用以预防多线程同时对一个线程敏感的代码块的访问.
- Synchronization Event.用以让一个线程通过事件来与另一个线程进行通信.
- 它是一种含有两种状态的对象.
- 两种状态:signaled/un-signaled.
- 它用来激活和挂起线程.
- 等待un-signaled状态的同步事件,效果是挂起线程.
- 修改同步事件状态为signaled,效果是激活线程.
- 试图等待已经是signaled的同步事件的线程,会无延迟地继续执行代码.
- 它是一种含有两种状态的对象.
- 两种同步事件
- AutoResetEvent.从un-signaled转变为signaled状态时,自动激活一个线程.
- 自动重置自己的状态.类似于旋转门,当它变为signaled时允许一个线程通过.
- ManualResetEvent.允许激活N个线程.仅当其Reset()方法被调用时,才会回到un-signaled状态.
- Mutex和Semaphore都继承自WaitHandle.
- 调用WaitOne/WaitAny/WaitAll方法让线程等待同步事件的发生.
- 同步事件的Set方法被调用时,状态转变为signaled.
-
1 using System; 2 using System.Threading; 3 4 class ThreadingExample 5 { 6 static AutoResetEvent autoEvent; 7 8 static void DoWork() 9 { 10 Console.WriteLine(" worker thread started, now waiting on event..."); 11 autoEvent.WaitOne(); 12 Console.WriteLine(" worker thread reactivated, now exiting..."); 13 } 14 15 static void Main() 16 { 17 autoEvent = new AutoResetEvent(false); 18 19 Console.WriteLine("main thread starting worker thread..."); 20 Thread t = new Thread(DoWork); 21 t.Start(); 22 23 Console.WriteLine("main thread sleeping for 1 second..."); 24 Thread.Sleep(1000); 25 26 Console.WriteLine("main thread signaling worker thread..."); 27 autoEvent.Set(); 28 } 29 }
- Mutex
- 功能上类似于Monitor,用以预防多线程同时对同一代码块的访问.
- mutex是一个同步原语.一个线程获得了mutex后,其它想要获取mutex的线程必须等到直到第一个线程释放了mutex.
- mutex会使用更多的系统资源,可以用来同步不同进程内的线程.可以穿过应用Domain的边界.
- Mutex类继承自WaitHandle.当调用.当一个线程调用WaitOne()来请求一个mutex的拥有权时,会被阻塞直到以下的事件发生.
- mutex变为signaled状态来指示自己现在没有拥有者.
- 此时,WaitOne()返回true.调用线程获取mutex的拥有权,并可以访问该mutex保护的资源.
- 该线程访问资源完毕后,必须调用ReleaseMutex()来释放mutex的拥有权.
- 指定的间隔时间已过.
- 此时,WaitOne()返回false.调用线程不会获取mutex的拥有权.
- 代码必须针对不能访问mutex资源时的情况进行处理.
- mutex变为signaled状态来指示自己现在没有拥有者.
- 强制的线程identity.
- mutex只能被获取它的线程来释放.
- 线程释放一个它不拥有的mutex会抛出ApplicationException.
- Semapore不会执行线程identity.
- mutex可以穿过应用Domain边界.
- 重复执行
- 一个线程可以对同一个mutex多次调用WaitOne方法,而不会被阻塞(在获取后).
- 同时,必须调用相同次数的ReleaseMutex().
- Abandon mutex
- 当mutex的拥有者线程被中止时,该mutex称为遗弃mutex.
- 遗弃mutex是signaled状态.下一个等待线程会获取该mutex的拥有权.
- 在2.0以后的Framework版本里,会抛出AbandonedMutexException异常(当下一个线程获得其拥有权时).
- 它通常意味着代码错误,可能会造成数据结构的破坏.
- 下一个获取mutex拥有权的线程,在可能的情况下,应该处理该异常并保证数据结构的正确性.
- 对于一个系统级别的遗弃mutex,可能指示了一个应用被突然中止.
- 类型.
- 本地的非命名的mutex.仅存在于当前进程中.
- 每一个未命名的Mutex对象代表一个单独的本地mutex.
- 命名的系统级mutex.
- 在OS内可见,可用来同步现存的活动的进程.
- 系统级别的命名mutex,同名的只有一个.使用OpenExisting()来打开一个既存的命名系统mutex.
- 在一个运行着终端服务的Server上,系统mutex有两种可见性
- "Global"前缀的.在所有的终端Server会话中都可见.
- "Local"前缀的.仅在创建它的终端Server会话中可见.
- 默认是"Local".
- 同名的Global/Local可以同时存在.该范围描述的是Session范围,而不是Process范围(在Session内的所有Process都可见).
- 本地的非命名的mutex.仅存在于当前进程中.
- InterLock
- 提供了对多线程共享的变量的atomic操作(増,减,对比,交互).
- 一个增减操作不是atomic.
- 从一个实例变量中加载一个value到register中.
- 増/减该value.
- 将值存储到实例变量中去.
- 如果第一个线程正在进行三个步骤(例如到第二个步骤),而此时,其他线程抢先执行,修改了该value.然后第一个线程恢复执行时,会把其他线程对value的修改覆盖掉.
- Semaphore
- 构造时指定一个计数,表明最多有多少个线程可以同时进入Lock状态.
- 不保证等待线程进入Semaphore的顺序.
- 不保证线程identify.
- WaitOne()一次进入的线程,可以调用Release(2),然后其它线程再调用Release时异常.
- 同样,含有本地和全局的Semaphore.
- Signal.
- 等待另一个线程的信号的最简单方式是调用join()来等待一个线程执行完毕后,得到通知.
- ReaderWriterLock/ReaderWriterLockSlim.
- 当对资源进行写操作时,需要锁定.而允许在不进行写操作时,多线程对同一资源的同时读访问.
- 对于不经常被修改的资源,相对于其他One-at-a-time锁定,它提高了吞吐量.
- ReaderWriterLockSlim
- Slim拥有更简洁的规则,针对重复,增加,减少锁定状态.很多情况下避免了死锁的发生,并且拥有更好的性能,是推荐的做法.
- 默认情况下,其实例都使用NoRecursion标志来代表不能进行递归.
- 递归会引入复杂度,并且更易导致死锁.
- 当从现有项目中的Lock/Monitor/ReaderWriterLock升级时,使用SupportRecursion来支持递归.
- 线程可以以三种状态进入Lock:读/写/可升级(到写)的读.
- 只能有一个线程处于读Lock,并且此时任何线程都不能处于3种状态中的一种.
- 只能有一个线程处于可升级的读.
- 可以有多个线程处于读Lock.并且此时可以有一个线程处于可升级读Lock.
- 会处理线程affinity.
- 每一个线程必须自己调用方法来进入和退出Lock状态.
- 任何线程都不能更改其他线程的Lock状态.
- Up/DownGrading.
- 适用于一个经常读取保护资源的线程,在满足某种情况下,需要对资源进行写操作.
- 处于可升级读Lock的线程,拥有对保护资源的读权限,并且可以调用(Try)EnterWriteLock()来升级为写Lock.
- 非递归Lock情况下.一个处于读Lock状态的线程不能直接变为可升级读Lock.因为这样可能会导致多个线程之间的死锁.
- 当有其他的读Lock线程时,可升级读Lock线程会被阻塞.其他试图获取读Lock的线程也会被阻塞.
- 当所有的读Lock线程都释放Lock后,可升级读Lock线程升级到写Lock模式.
- 处于可升级读Lock状态的线程可以无限地升级/降级(与写Lock之间).只要它是唯一一个对保护资源写的线程.
- 通过调用EnterReadLock+ExitUpgradableReadLock方法,可以降级到读Lock.
- 但是,一旦降级到读Lock,就不能重入到可升级读Lock状态.除非退出读Lock状态后.
- LockSlim可以处于四种状态
- Not entered.试图获取任意Lock状态的线程,都会得到该Lock.
- Read.只会Block试图获取读Lock状态的线程.
- Upgrade.如果线程正在等待写Lock,那么会被Block,否则会允许进入读Lock.其余两种Lock状态的进入尝试会被Block.
- Write.Block所有尝试进入Lock状态的线程.
- 当一个线程退出Lock而导致了状态变更.那么按以下的顺序唤醒线程
- 已处于可升级读Lock状态并且在等待读Lock的线程.
- 等待写Lock的.
- 等待可升级读Lock的.
- 等待读Lock的.
- 可递归的Lock策略下,一个线程可以进入以下的状态.
- 处于读Lock模式的线程可以递归地进入读Lock模式.但是不能进入另外两种模式.
- 处于可升级读Lock模式的线程可以递归地进入3种状态.
- 处于写Lock模式的线程可以递归地进入3种状态.
- 没有进入Lock模式的线程,可以进入3种状态中的任一种,只是可能会被Block.
- 长时间占有读/写Lock会饿死其它线程.使用读写锁的场景下,应该尽量减少写锁定占用的时间.
- 一个线程可以占有读/写Lock,但是不能同时占有两个.
- 从读Lock转变为写Lock:UpgradeToWriterLock/DowngradeFromWriterLock.而不必先释放再获取.
- 递归Lock需要增加Lock上的lock计数.
- 两个队列:读/写.
- 当一个线程释放了读Lock.读队列中的所有线程瞬间获得读Lock.
- 当所有的读Lock线程都释放了锁定后,写队列的第一个等待线程获得读Lock.
- 所以,Lock会交替地在读/写两个队列中选择执行权.
- 当有一个线程等待读Lock释放,以获得写Lock时.
- 新的读Lock请求线程会被列入读Lock队列中.
- 虽然允许同时的读Lock请求,但是这样做是为了防止写线程会受到不确定(也就是可能非常长时间)的阻塞.这是一种对Writer有利的策略.
- 超时时间,
- 为了避免死锁的出现,在尝试获取Lock时,可以指定超时时间.
- -1.代表没有超时时间,线程会一直等待下去直到获取了期望的Lock.
- 0,代表立刻.如果现在获取不了期望的Lock,那么直接返回.并抛出ApplicationException.
- >0.等待指定的毫秒.
- <-1的值,会直接被认为是0.
- 可以使用TimeSpan来作为参数来指定时间间隔.1秒=1000毫秒=10,000,000纳秒.