CLR Via C# 3rd 阅读摘要 -- Chapter 28 – Primitive Thread Synchronization Constructs

Class Libraries and Thread Safety

  1. 线程同步是用来避免多个线程同时访问共享数据时出现冲突;
  2. 线程同步的障碍:
    • 1.极其乏味易错;
    • 2.锁严重影响性能;
    • 3.线程同步锁在同一时间点仅允许一个线程访问资源。
  3. 设计程序时应该尽可能的避免线程同步,最好避免采用static字段的共享数据;
  4. 尝试使用值类型,因为它们总是拷贝传递的,因此每个线程在自身拥有的拷贝上操作。所以当多个线程同时以只读方式访问值类型共享数据时是安全的。
  5. FCL保证所有的静态方法是线程安全的;
  6. 当一个线程构造一个对象,只有该线程拥有该对象的引用,其他线程不能访问该对象;
  7. 类型设计时应遵循的模式:确保所有的静态方法多线程安全、所有的实例方法多线程不安全;
    一个例外:如果实例方法是用于协调多线程的,那么该实例方法也应该多线程安全。如CancellationTokenSource.Cancel方法。

Primitive User-Mode and Kernel-Mode Constructs

  1. 两种原生线程同步结构:
    • 用户模式:速度很快,使用特定的CPU指令协调线程,协调工作由硬件完成。Windows系统不会检测线程是否阻塞在用户模式同步结构;
      线程池线程阻塞在用户模式同步结构不会被当成阻塞,线程池不会创建新的线程来代替临时阻塞的线程。
      采用该模式的线程会被系统抢占调度,可能导致线程被反复快速调度,从而会浪费CPU;
    • 核心模式:Windows操作系统提供,调用实现在系统内核的函数。当一个线程使用内核模式同步结构来请求其他线程持有的资源,Windows将阻塞该线程所以不会浪费CPU;
      线程在用户模式与核心模式间互相转换会严重损害性能;
    • 如果一个线程持有一个同步结构不再释放它,等待该结构的线程将永远被阻塞:
      • 活锁:如果该同步结构是用户模式,线程一直运行在CPU上;
      • 死锁:如果该同步结构是核心模式,线程被阻塞住;
    • 活锁与死锁都很糟糕,但相比之下,活锁更糟糕,因为活锁既浪费CPU又浪费内存,而死锁只浪费内存;

User-Mode Constructs

  1. 原生的用户模式结构:
    • 易变(volatile)结构:在一个简单数据类型变量上执行原子读或写操作;
    • 联锁(Interlocked)结构:在一个简单数据类型变量上执行原子读和写操作;
  2. 易变结构确认读或写操作是否原子的非常重要,它们还控制这些原子操作的时机。联锁结构执行操作要比简单的读和写操作复杂一些,它们也需要控制操作的时机;
  3. 比如一个Int64的变量如果没有正确对齐,那么当多线程读写该变量时,可能会出现只正确读取前四字节或后四字节,这就是脏读(torn read)?
  4. System.Threading.Thread.VolatileWrite(), .VolatileRead(), .MemoryBarrier()静态方法通常禁止C#编译器、JIT编译器、CPU进行优化;
  5. 当线程通过共享数据进行通信时,对最后一个值的写操作调用VolatileWrite(),对第一个值的读操作调用VolatileRead();
  6. C# volatile关键字:JIT编译器会对标记为volatile的字段的读写进行VolatileWrite()和VolatileRead()处理;
  7. 将来,C#不支持通过引用传递volatile字段到方法调用中;
  8. 现在,Interlocked.Exchange(), .CompareExchange()只支持简单值类型,将来还会提供对Object, IntPtr, Single, Double以及泛型版本的支持;
  9. SimpleSpinLock的缺陷,当锁出现竞争时,线程会不停轮转,浪费CPU资源:
    internal struct SimpleSpinLock {
        private Int32 m_ResourceInUse;  //0 = false (default), 1 = true
        
        public void Enter() {
            // Set the resource to in-use and if this thread
            // changed it from Free, then return
            while (Interlocked.Exchange(ref m_ResourceInUse, 1) != 0) {
                // Black Magic goes here...
            }
        }
        
        public void Leave() {
            // Mark the resource as Free
            Thread.VolatileWrite(ref m_ResourceInUse, 0);
        }
    }
  10. FCL提供的System.Threading.SpinWait
  11. Thread.Sleep(),允许线程资源放弃时间片,休眠时间是近似值;
  12. Thread.Yield(),告诉Windows在当前CPU上调度其他线程。如果当前CPU上有其他线程,返回true并结束调用线程的时间片,被调度线程结束自身的时间片之后,调用线程继续。
  13. Thead.SpinWait(),线程强制自己暂停,允许超线程CPU切换到其他线程。该方法会使用特别的CPU指令,如果CPU不支持超线程,该指令被忽略;
  14. Win32 API:Sleep(), SwitchToThread(), YieldProcessor();
  15. 当集合中的每一项都需要关联锁的时候,轻量级、内存友好的SpinLock是个好东西。但是要注意不要传递它的实例,因为是值类型,传递的是拷贝;

Kernel-Mode Constructs

  1. 核心模式的同步结构比用户模式的要慢许多,因为它们由Windows操作系统协调,此外每个核心对象上的方法调用都会导致调用线程从托管代码转换到本地用户模式代码再到本地核心模式代码,然后再原路返回;
  2. 核心模式同步结构提供哪些用户模式所没有的优点:
    • 当核心模式同步结构检测到资源上出现竞争时,Windows阻塞竞争失败的线程,因此不再浪费处理器资源;
    • 核心模式同步结构可以同步本地线程和托管线程;
    • 核心模式同步结构可以同步运行在同一台机器上不同处理器上的线程;
    • 核心模式同步结构可以附上安全限制避免未经认证的帐号访问他们;
    • 线程能够被阻塞直到所有的核心模式同步结构都可用,或者任何一个可用;
    • 阻塞在核心模式同步结构上的线程可以设置一个timeout值;如果这段时间内没能获得期望的资源,那么不再阻塞以干点其他正事。
  3. 原生的核心模式结构:
    • 事件(Events):事件是有内核管理的Boolean变量。当事件为false时,线程被阻塞。有两种事件:AutoResetEventManualResetEvent
    • 信号量(Semaphore):信号量是内核管理的Int32变量。当信号量为0时,线程被阻塞;>0时,线程不被阻塞。
  4. 核心模式结构的层次结构:
      WaitHandle
      |--EventWaitHandle
      |--|--AutoResetEvent
      |--|--ManualResetEvent
      |--Semaphore
      |--Mutex
  5. 每个在核心模式同步结构上的方法调用都会出现完全内存保护;
  6. SimpleWaitLock vs SimpleSpinLock
    当锁上没有竞争时,SimpleWaitLockSimpleSpinLock要慢许多,因为SimpleWaitLockEnterLeave方法会强制调用线程在托管代码到核心代码之间进行来回转换;
    但是当锁上有竞争时,SimpleWaitLock不会浪费CPU资源,而SimpleSpinLock会不停的轮转CPU;
  7. AutoResetEventManualResetEventSemaphore行为比较:
    • 当多个线程在AutoResetEvent上等待时,设置事件只会让一个线程不再被阻塞;
    • 当多个线程在ManualResetEvent上等待时,设置事件会让所有线程不再被阻塞;
    • 当多个线程在Semaphore上等待时,释放信号量会让releaseCount个线程不再被阻塞(relaseCountSemaphore.Release()方法的参数)。
  8. 互斥体(Mutex)表现为一个同斥锁;
  9. Mutex很像AutoResetEvent或者Semaphore(count = 1),但有一些更复杂的逻辑:
    • 首先,Mutex对象通过查询调用线程的ID记录了哪个线程获得了它。当线程调用ReleaseMutexMutex会确认是否为同一线程;
    • 其次,Mutex维护一个递归计数指出被获得了多少次。只有当递归计数降为0时,其他线程才能获取。
  10. Mutex对象需要更多的内存,并且需要维护额外信息,所以会比较慢;
  11. 如何当一个单独的核心结构可用时调用方法?使用System.Threading.ThreadPool.RegisterWaitForSingleObject静态方法。

本章小结

    本章讲述了原生的线程同步结构,首先介绍了类库和线程安全性概念,然后对线程同步模式进行了分类:用户模式与核心模式,接着详细说明了这两种同步模式的实现细节,并举例进行了对比。

posted @ 2010-07-01 10:55  bengxia  阅读(485)  评论(0编辑  收藏  举报
无觅相关文章插件,快速提升流量