多线程下的资源同步访问

  • 在一个应用程序中使用多线程
    • 好处是每一个线程异步地执行.
      • 对于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 }
        View Code

         

      • 一般使用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方法必须在同步块内调用.
    • 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 }
      MSDN Example

       

  • Mutex
    • 功能上类似于Monitor,用以预防多线程同时对同一代码块的访问.
      • mutex是一个同步原语.一个线程获得了mutex后,其它想要获取mutex的线程必须等到直到第一个线程释放了mutex.
      • mutex会使用更多的系统资源,可以用来同步不同进程内的线程.可以穿过应用Domain的边界.
    • Mutex类继承自WaitHandle.当调用.当一个线程调用WaitOne()来请求一个mutex的拥有权时,会被阻塞直到以下的事件发生.
      • mutex变为signaled状态来指示自己现在没有拥有者.
        • 此时,WaitOne()返回true.调用线程获取mutex的拥有权,并可以访问该mutex保护的资源.
        • 该线程访问资源完毕后,必须调用ReleaseMutex()来释放mutex的拥有权.
      • 指定的间隔时间已过.
        • 此时,WaitOne()返回false.调用线程不会获取mutex的拥有权.
        • 代码必须针对不能访问mutex资源时的情况进行处理.
    • 强制的线程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都可见).
  • 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纳秒.
posted @ 2014-07-02 15:25  robynhan  阅读(760)  评论(0编辑  收藏  举报