第2章
线 程 同 步
在本章中我们将讲述一些关于在多线程中使用共享资源的常用技术。你将学到以下内容:
执行基本的原子操作
使用Mutex类
使用SemaphoreSlim类
使用AutoResetEvent类
使用ManualResetEventSlim类
使用CountDownEvent类
使用Barrier类
使用ReaderWriterLockSlim类
使用SpinWait类
2.1 简介
正如第1章中所看到的一样,多个线程同时使用共享对象会造成很多问题。同步这些线程使得对共享对象的操作能够以正确的顺序执行是非常重要的。在1.10节,我们遇到了一个叫作竞争条件的问题。导致这问题的原因是多线程的执行并没有正确同步。当一个线程执行递增和递减操作时,其他线程需要依次等待。这种常见问题通常被称为线程同步。
有多种方式来实现线程同步。首先,如果无须共享对象,那么就无须进行线程同步。令人惊奇的是大多数时候可以通过重新设计程序来除移共享状态,从而去掉复杂的同步构造。请尽可能避免在多个线程间使用单一对象。
如果必须使用共享的状态,第二种方式是只使用原子操作。这意味着一个操作只占用一个量子的时间,一次就可以完成。所以只有当前操作完成后,其他线程才能执行其他操作。因此,你无须实现其他线程等待当前操作完成,这就避免了使用锁,也排除了死锁的情况。
如果上面的方式不可行,并且程序的逻辑更加复杂,那么我们不得不使用不同的方式来协调线程。方式之一是将等待的线程置于阻塞状态。当线程处于阻塞状态时,只会占用尽可能少的CPU时间。然而,这意味着将引入至少一次所谓的上下文切换(context switch)。上下文切换是指操作系统的线程调度器。该调度器会保存等待的线程的状态,并切换到另一个线程,依次恢复等待的线程的状态。这需要消耗相当多的资源。然而,如果线程要被挂起很长时间,那么这样做是值得的。这种方式又被称为内核模式(kernel-mode),因为只有操作系统的内核才能阻止线程使用CPU时间。
万一线程只需要等待一小段时间,最好只是简单的等待,而不用将线程切换到阻塞状态。虽然线程等待时会浪费CPU时间,但我们节省了上下文切换耗费的CPU时间。该方式又被称为用户模式(user-mode)。该方式非常轻量,速度很快,但如果线程需要等待较长时间则会浪费大量的CPU时间。
为了利用好这两种方式,可以使用混合模式(hybrid)。混合模式先尝试使用用户模式等待,如果线程等待了足够长的时间,则会切换到阻塞状态以节省CPU资源。
在本章中我们将介绍线程同步这一知识点。我们将讲解如何执行原子操作,以及如何使用.NET框架中现有的同步方式。