同步要领下面的表格列展了.NET对协调或同步线程动作的可用的工具:
简易阻止方法
构成 |
目的 |
Sleep |
阻止给定的时间周期 |
Join |
等待另一个线程完成 |
锁系统
构成 |
目的 |
跨进程? |
速度 |
lock |
确保只有一个线程访问某个资源或某段代码。 |
否 |
快 |
Mutex |
确保只有一个线程访问某个资源或某段代码。 可被用于防止一个程序的多个实例同时运行。 |
是 |
中等 |
Semaphore |
确保不超过指定数目的线程访问某个资源或某段代码。 |
是 |
中等 |
(同步的情况下也提够自动锁。)
信号系统
构成 |
目的 |
跨进程? |
速度 |
EventWaitHandle |
允许线程等待直到它受到了另一个线程发出信号。 |
是 |
中等 |
Wait 和 Pulse* |
允许一个线程等待直到自定义阻止条件得到满足。 |
否 |
中等 |
非阻止同步系统*
构成 |
目的 |
跨进程? |
速度 |
Interlocked* |
完成简单的非阻止原子操作。 |
是(内存共享情况下) |
非常快 |
volatile* |
允许安全的非阻止在锁之外使用个别字段。 |
非常快 |
* 代表页面将转到第四部分
阻止 (Blocking)当一个线程通过上面所列的方式处于等待或暂停的状态,被称为被阻止。一旦被阻止,线程立刻放弃它被分配的CPU时间,将它的ThreadState属性添加为WaitSleepJoin状态,不在安排时间直到停止阻止。停止阻止在任意四种情况下发生(关掉电脑的电源可不算!):
- 阻止的条件已得到满足
- 操作超时(如果timeout被指定了)
- 通过Thread.Interrupt中断了
- 通过Thread.Abort放弃了
当线程通过(不建议)Suspend 方法暂停,不认为是被阻止了。
休眠 和 轮询调用Thread.Sleep阻止当前的线程指定的时间(或者直到中断):
static void Main() {
Thread.Sleep (0); // 释放CPU时间片Thread.Sleep (1000); // 休眠1000毫秒Thread.Sleep (TimeSpan.FromHours (1)); // 休眠1小时Thread.Sleep (Timeout.Infinite); // 休眠直到中断}更确切地说,Thread.Sleep放弃了占用CPU,请求不在被分配时间直到给定的时间经过。Thread.Sleep(0)放弃CPU的时间刚刚够其它在时间片队列里的活动线程(如果有的话)被执行。
Thread.Sleep在阻止方法中是唯一的暂停汲取Windows Forms程序的Windows消息的方法,或COM环境中用于单元模式。这在Windows Forms程序中是一个很大的问题,任何对主UI线程的阻止都将使程序失去相应。因此一般避免这样使用,无论信息汲取是否被“技术地”暂定与否。由COM遗留下来的宿主环境更为复杂,在一些时候它决定停止,而却保持信息的汲取存活。微软的 Chris Brumm 在他的博客中讨论这个问题。(搜索: 'COM "Chris Brumme"')
线程类同时也提供了一个SpinWait方法,它使用轮询CPU而非放弃CPU时间的方式,保持给定的迭代次数进行“无用地繁忙”。50迭代可能等同于停顿大约一微秒,虽然这将取决于CPU的速度和负载。从技术上讲,SpinWait并不是一个阻止的方法:一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,并且也不会被其它的线程过早的中断(Interrupt)。SpinWait很少被使用,它的作用是等待一个在极短时间(可能小于一微秒)内可准备好的可预期的资源,而不用调用Sleep方法阻止线程而浪费CPU时间。不过,这种技术的优势只有在多处理器计算机:对单一处理器的电脑,直到轮询的线程结束了它的时间片之前,一个资源没有机会改变状态,这有违它的初衷。并且调用SpinWait经常会花费较长的时间这本身就浪费了CPU时间。
阻止 vs. 轮询线程可以等待某个确定的条件来明确轮询使用一个轮询的方式,比如:
while (!proceed);或者:
while (DateTime.Now < nextStartTime);这是非常浪费CPU时间的:对于CLR和操作系统而言,线程进行了一个重要的计算,所以分配了相应的资源!在这种状态下的轮询线程不算是阻止,不像一个线程等待一个EventWaitHandle(一般使用这样的信号任务来构建)。
阻止和轮询组合使用可以产生一些变换:
while (!proceed) Thread.Sleep (x); // "轮询休眠!"x越大,CPU效率越高,折中方案是增大潜伏时间,任何20ms的花费是微不足道的,除非循环中的条件是极其复杂的。
除了稍有延迟,这种轮询和休眠的方式可以结合的非常好。(但有并发问题,在第四部分讨论)可能它最大的用处在于程序员可以放弃使用复杂的信号结构 来工作了。
使用Join等待一个线程完成你可以通过Join方法阻止线程直到另一个线程结束:
class JoinDemo {
static void Main() {
Thread t = new Thread (delegate() { Console.ReadLine(); });t.Start();t.Join(); // 等待直到线程完成Console.WriteLine ("Thread t's ReadLine complete!");}}Join方法也接收一个使用毫秒或用TimeSpan类的超时参数,当Join超时是返回false,如果线程已终止,则返回true 。Join所带的超时参数非常像Sleep方法,实际上下面两行代码几乎差不多:
Thread.Sleep (1000);Thread.CurrentThread.Join (1000);(他们的区别明显在于单线程的应用程序域与COM互操作性,源于先前描述Windows信息汲取部分:在阻止时,Join保持信息汲取,Sleep暂停信息汲取。)
锁和线程安全锁实现互斥的访问,被用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:
class ThreadUnsafe { static int val1, val2; static void Go() { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; }}这不是线程安全的:如果Go方法被两个线程同时调用,可能会得到在某个线程中除数为零的错误,因为val2可能被一个线程设置为零,而另一个线程刚好执行到if和Console.WriteLine语句。
下面用lock来修正这个问题:
class ThreadSafe { static object locker = new object(); static int val1, val2; static void Go() { lock (locker) { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } }}在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。互斥锁有时被称之对由锁所保护的内容强迫串行化访问,因为一个线程的访问不能与另一个重叠。在这个例子中,我们保护了Go方法的逻辑,以及val1 和val2字段的逻辑。
一个等候竞争锁的线程被阻止将在ThreadState上为WaitSleepJoin状态。稍后我们将讨论一个线程通过另一个线程调用Interrupt或Abort方法来强制地被释放。这是一个相当高效率的技术可以被用于结束工作线程。
C#的lock 语句实际上是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法:
Monitor.Enter (locker);try {
if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0;}finally { Monitor.Exit (locker); } 在同一个对象上,在调用第一个之前Monitor.Enter而先调用了Monitor.Exit将引发异常。
Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false,因为超时了。TryEnter也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。
选择同步对象任何对所有有关系的线程都可见的对象都可以作为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。服从这些规则,同步对象可以兼对象和保护两种作用。比如下面List :
class ThreadSafe { List <string> list = new List <string>(); void Test() { lock (list) {list.Add ("Item 1");...一个专门字段是常用的(如在先前的例子中的locker) , 因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:
lock (this) { ... }或:
lock (typeof (Widget)) { ... } // 保护访问静态是不好的,因为这潜在的可以在公共范围访问这些对象。
锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x) 而被阻止,