zoukankan      html  css  js  c++  java
  • 多线程下的资源同步访问

    • 在一个应用程序中使用多线程
      • 好处是每一个线程异步地执行.
        • 对于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纳秒.
  • 相关阅读:
    PAT 甲级 1072 Gas Station (30 分)(dijstra)
    python flask框架学习(二)——第一个flask程序
    python flask框架学习(一)——准备工作和环境配置与安装
    【Beats】Filebeat工作原理(十七)
    【Beats】Filebeat介绍及使用(十六)
    Dapper简介
    Sequelize ORM
    StackExchange / Dapper
    TypeORM 简介
    Egg.js 是什么?
  • 原文地址:https://www.cnblogs.com/robyn/p/3820378.html
Copyright © 2011-2022 走看看