编写线程应用程序时,可能需要使各单个线程与程序的其他部分同步。同步可以在多线程编程的非结构化特性与同步处理的结构化顺序之间进行平衡。同步技术具有以下用途:
— 如果必须以特定顺序执行任务,那么使用同步技术可以显式控制代码运行的顺序。
— 使用同步技术可以避免当两个线程同时共享同一资源时可能会发生的问题。
.NET框架提供了两种方法可以实现同步,即简单方法和高级方法。简单方法包括轮询和等待,高级方法使用同步对象。
轮询通过循环重复地检查异步调用的状态。轮询是效率最低的线程管理方法,因为它重复地检查各个线程属性的状态,因而浪费了大量资源。例如,可以使用IsAlive属性来轮询检查线程是否已退出。使用该属性时应谨慎,因为处于活动状态的线程并不一定正在运行。
可以使用线程的ThreadState属性来获取有关线程状态的详细信息。因为线程可以在任意给定时间具有多种状态,所以存储在ThreadState中的值可以是System.Threading.ThreadState枚举中值的组合。因此,轮询时应仔细检查所有相关线程的状态。例如,如果某个线程的状态指示它不是Running,该线程可能已完成。但该线程也可能处于挂起或休眠状态。
通过轮询来实现对线程运行顺序的控制将牺牲多线程处理的许多优点。更有效的方法是使用Thread类的Join方法控制线程。Join方法对于在启动另一任务之前确定线程是否已完成很有用。Join方法在线程结束前等待一段指定的时间。如果线程在超时之前结束,则Join返回True;否则返回False。Join使调用过程在线程完成或调用超时(如果指定了超时的话)之前处于等待状态。之所以称其为“Join”,是因为创建新线程是执行路径中的一个分支。使用Join可以重新将单独的执行路径合并为一个线程。
图19-1显示了下面的程序的执行过程:
// Join.cs
// Join示例
using System;
using System.Threading;
public class Test
{
// Main启动主线程,称之为线程1
public static void Main()
{
Console.WriteLine("进入线程1");
Console.WriteLine("启动线程2");
Thread thread2 = new Thread(new ThreadStart(ThreadProc2));
thread2.Start();
Console.WriteLine("启动线程3");
Thread thread3 = new Thread(new ThreadStart(ThreadProc3));
thread3.Start();
Console.WriteLine("Join线程2");
thread2.Join();
Console.WriteLine("Join线程3");
thread3.Join();
}
static void ThreadProc2()
{
Console.WriteLine("进入线程2");
for (int i = 1; i < 4; ++i)
{
Thread.Sleep(50);
Console.WriteLine("\t+++++++线程2+++++++++");
}
Console.WriteLine("退出线程2");
}
static void ThreadProc3()
{
Console.WriteLine("进入线程3");
for (int i = 1; i < 8; ++i)
{
Thread.Sleep(50);
Console.WriteLine("\t-------线程3---------");
}
Console.WriteLine("退出线程3");
}
}
图19-1 用Join方法来控制线程同步
Join是一个同步调用或阻止调用。调用Join或等待句柄的等待方法后,调用过程即会停止,并等待该线程发出信号指示它已经完成。上述程序的输出很好地说明了这一点:
进入线程1
启动线程2
进入线程2
启动线程3
进入线程3
Join线程2
+++++++线程2+++++++++
-------线程3---------
+++++++线程2+++++++++
-------线程3---------
+++++++线程2+++++++++
退出线程2
Join线程3
-------线程3---------
-------线程3---------
-------线程3---------
-------线程3---------
-------线程3---------
退出线程3
简单方法不但低效,而且不太可靠,只适合于管理少量线程的情况,不适合于管理大型项目。对于大型项目来说,应该利用使用同步对象的高级技术。
.NET框架提供了一系列同步类来控制线程的同步,最常用的同步类包括Interlocked、Monitor和Mutex。
线程同步是一种协作,所有使用共享资源(通常把这种资源叫做受保护资源)的线程都必须遵守同步机制。只要有一个线程不遵守同步机制而直接访问受保护资源,同步机制就可能失效。
当多个并行运行的线程需要访问受保护资源时,使它们保持同步是非常重要的,否则就可能出现死锁和竞争条件,从而导致线程不能继续执行或者得到不正确的运行结果。
19.3.1 使用Interlocked
Interlocked类是一种互锁操作,提供对多个线程共享的变量进行同步访问的方法。如果线程共享的变量位于共享内存中,那么不同进程的线程就可以使用Interlocked类对象来进行同步。互锁操作具有原子性,即整个操作是不能由相同变量上的另一个互锁操作所中断的单元。在抢先式多线程操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改和存储该值之前被挂起,因此互锁操作对于抢先多线程操作系统非常重要。
Interlocked类提供了以下功能:
— 在.NET框架2.0中,Add方法向变量添加一个整数值并返回该变量的新值。
— 在.NET框架2.0中,Read方法作为一个原子操作读取一个64位整数值。这在32位操作系统上是有用的,在32位操作系统上,读取一个64位整数通常不是一个原子操作。
— Increment和Decrement方法递增或递减某个变量,并返回结果值。
— Exchange方法执行指定变量的值的原子交换,返回该值并将其替换为新值。在.NETFramework 2.0中,可以使用此方法的一个泛型重载对任何引用类型的变量执行这种交换。
— CompareExchange方法也交换两个值,但是根据比较的结果而进行操作。在.NET框架 2.0中,可以使用此方法的一个泛型重载对任何引用类型的变量执行这种交换。
在现代处理器中,Interlocked类的方法经常可以由单个指令来实现。因此,它们提供了性能非常高的同步,并且可用于构建更高级的同步机制。
可以使用Interlocked类来解决生产者-消费者关系中的竞争条件问题。在下面的程序中,标志bufferEmpty用来表示共享缓冲区是否为空。只有当共享缓冲区为空时,生产者才能向共享缓冲区中存入数据;只有当共享缓冲区为满时,消费者才能从共享缓冲区中取出数据。标志bufferEmpty的读取或修改分别使用Interlocked.Read或Interlocked.Increment、Interlocked. Decrement方法来进行,因此这个标志在任何时刻都只能被一个线程访问或修改。
// Interlocked.cs
// Interlocked示例
using System;
using System.Threading;
class Test
{
private long bufferEmpty = 0;
private string buffer = null;
static void Main()
{
Test t = new Test();
// 进行测试
t.Go();
}
public void Go()
{
Thread t1 = new Thread(new ThreadStart(Producer));
t1.Name = "生产者线程";
t1.Start();
Thread t2 = new Thread(new ThreadStart(Consumer));
t2.Name = "消费者线程";
t2.Start();
// 等待两个线程结束
t1.Join();
t2.Join();
}
// 生产者方法
public void Producer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
try
{
for (int j = 0; j < 16; ++j)
{
// 等待共享缓冲区为空
while (Interlocked.Read(ref bufferEmpty) != 0)
Thread.Sleep(100);
// 构造共享缓冲区
Random r = new Random();
int bufSize = r.Next() % 64;
char[] s = new char[bufSize];
for (int i = 0; i < bufSize; ++i)
{
s[i] = (char)((int)'A' + r.Next() % 26);
}
buffer = new string(s);
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
buffer);
// 互锁加一,成为1,标志共享缓冲区已满
Interlocked.Increment(ref bufferEmpty);
// 休眠,将时间片让给消费者
Thread.Sleep(10);
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
}
}
// 消费者方法
public void Consumer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
try
{
for (int j = 0; j < 16; ++j)
{
while (Interlocked.Read(ref bufferEmpty) == 0)
Thread.Sleep(100);
// 打印共享缓冲区
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
buffer);
// 互锁减一,成为0,标志共享缓冲区已空
Interlocked.Decrement(ref bufferEmpty);
// 休眠,将时间片让给生产者
Thread.Sleep(10);
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
}
}
}
上述程序的输出结果如下:
生产者线程:开始执行
生产者线程:YHPJVKTWJDIOTWMVPTPECIENLZZGRQOZMPSZKSNWZNNKBDVTJZKG
消费者线程:开始执行
消费者线程:YHPJVKTWJDIOTWMVPTPECIENLZZGRQOZMPSZKSNWZNNKBDVTJZKG
生产者线程:M
消费者线程:M
生产者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
消费者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
生产者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
消费者线程:QNVGDPMJYBAEUOVNASMWLPUPMKLFAQGTAICQSVDKJXEUAWZBSEXPFQBBT
生产者线程:EFLWFD
消费者线程:EFLWFD
生产者线程:HWUHZQZNHQBKKXMIOYYAQCXNJDCQHZOIGLLDVIFIXCFNZWCZWFYBBMPCWEYW
消费者线程:HWUHZQZNHQBKKXMIOYYAQCXNJDCQHZOIGLLDVIFIXCFNZWCZWFYBBMPCWEYW
生产者线程:ONHRFHDRJMPHBQITGJTXABXHBDSJQFSJHFGIIPEBLHBMXQAEITZB
消费者线程:ONHRFHDRJMPHBQITGJTXABXHBDSJQFSJHFGIIPEBLHBMXQAEITZB
生产者线程:Z
消费者线程:Z
生产者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
消费者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
生产者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
消费者线程:GWNOQMTEYKKXCLRLUIQPMIKJCOEFCIKDVYNZQSUPYUPWWGEMRBMHBYLVJ
生产者线程:RLDBSA
消费者线程:RLDBSA
生产者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
消费者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
生产者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
消费者线程:FWGSEIQSSBQKRZWOXPFBFOACVWGUXODYNIPVLCJWZMZFWQDCMUAQYNH
生产者线程:QLWI
消费者线程:QLWI
生产者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
消费者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
生产者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
消费者线程:XCJPMNGFHZIDSRIGIOCTRVQEWHSTJOSVBBZJTIWNMZQPVJHKVCNWXUZZMCQF
生产者线程:执行完毕
消费者线程:执行完毕
显然,我们得到了正确的结果:消费者得到的数据与生产者提供的数据相同。
19.3.2 使用Monitor和lock
Monitor类通过给单个线程授予对象锁来控制对象的访问。对象锁提供限制访问代码段(称为临界区)的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用Monitor类来确保不会允许其他的线程访问正在由锁的所有者执行的应用程序代码段,除非另一个线程正在使用其他的锁定对象执行该代码。
Monitor类具有以下功能:
— 它根据需要与某个对象相关联。
— 它是未绑定的,也就是说可以直接从任何上下文调用它。
— 不能创建Monitor类的实例。
Monitor类将维护每个同步对象的以下信息:
— 对当前持有锁的线程的引用。
— 对就绪队列的引用,它包含准备获取锁的线程。
— 对等待队列的引用,它包含正在等待锁定对象的状态发生变化的线程。
Monitor类通过使用静态方法Monitor.Enter、Monitor.TryEnter和Monitor.Exit来使特定对象获取锁和释放锁,以实现同步访问临界区的能力。在获取临界区的锁后,就可以使用静态方法Monitor.Wait、Monitor.Pulse和Monitor.PulseAll来与其他线程进行通信。这些方法的功能如表19-5所示。
表19-5 Monitor 类方法的功能
操 作 |
说 明 |
Enter TryEnter |
获取对象锁。此操作同样会标记临界区的开头。其他任何线程都不能进入临界区,除非它使用其他锁定对象执行临界区中的指令 |
Wait |
释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改 |
Pulse PulseAll |
向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态 |
Exit |
释放对象上的锁。此操作还标记受锁定对象保护的临界区的结尾 |
只能使用Monitor类来锁定引用类型(对象),而不能用于锁定值类型(值)。尽管可以向Monitor.Enter和Monitor.Exit传递值类型,但是每次调用值类型都会分别装箱。因此,每次调用都会创建一个不同的对象,传递给Monitor.Exit的对象不同于传递给Monitor.Enter的对象。所以,Monitor.Enter永远不会阻止,它要保护的代码并没有真正同步。由于传递给Monitor.Exit的对象不同于传递给Monitor.Enter的对象,Monitor.Monitor将引发SynchronizationLock Exception异常。
下面的程序使用Monitor类来同步生产者和消费者线程:
// Monitor.cs
// Monitor示例
using System;
using System.Threading;
class Test
{
private object synObj = new object();
private string buffer = null;
static void Main()
{
Test t = new Test();
// 进行测试
t.Go();
}
public void Go()
{
Thread t1 = new Thread(new ThreadStart(Producer));
t1.Name = "生产者线程";
t1.Start();
Thread t2 = new Thread(new ThreadStart(Consumer));
t2.Name = "消费者线程";
t2.Start();
// 等待两个线程结束
t1.Join();
t2.Join();
}
// 生产者方法
public void Producer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
for (int j = 0; j < 16; ++j)
{
try
{
// 进入临界区
Monitor.Enter(synObj);
// 构造共享缓冲区
Random r = new Random();
int bufSize = r.Next() % 64;
char[] s = new char[bufSize];
for (int i = 0; i < bufSize; ++i)
{
s[i] = (char)((int)'A' + r.Next() % 26);
}
buffer = new string(s);
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
buffer);
// 通知消费者数据已经准备好
Monitor.Pulse(synObj);
// 休眠,将时间片让给消费者
Thread.Sleep(10);
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
break;
}
finally
{
// 退出临界区
Monitor.Exit(synObj);
}
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
// 消费者方法
public void Consumer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
// 如果共享缓冲区为空,则休眠一秒,等待生产者线程构造缓冲区
if (buffer == null)
Thread.Sleep(1000);
for (int j = 0; j < 16; ++j)
{
try
{
// 进入临界区
Monitor.Enter(synObj);
// 打印共享缓冲区
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
buffer);
// 等待生产者的通知
Monitor.Wait(synObj, 1000);
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
break;
}
finally
{
// 退出临界区
Monitor.Exit(synObj);
}
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
}
当生产者将共享缓冲区填满后,就调用Monitor.Pulse方法通知消费者;当消费者取出了数据之后,就调用Monitor.Wait方法等待生成者的通知。注意,为了确保能从临界区中退出,应该在try语句的finally块中调用Monitor.Exit方法。
当一个线程A执行Monitor.Enter方法时,如果锁正被另一个线程B获得,那么线程A就会被阻塞,处于等待状态。在这种情形中,如果不想使线程A阻塞,就应该使用Monitor.TryEnter方法。
与Monitor.Enter方法不同,Monitor.TryEnter方法总会立即返回。如果锁未被其他线程获取,调用方法的线程就会获得锁,Monitor.TryEnter方法立即返回true值;如果锁已经被其他线程获取,调用方法的线程就不能获得锁,Monitor.TryEnter方法立即返回false值。因此,Monitor.TryEnter方法的基本调用方式如下:
if (Monitor.TryEnter(obj))
{
// 获得锁,访问临界区
// ……
}
else
{
// 没有获得锁,以后再试
// ……
}
为了方便Monitor类的使用,并确保能够从临界区中退出,C#提供了lock语句。lock语句的基本形式如下:
lock (【要锁定的引用类型对象】)
{
【临界区语句块】
}
lock语句将【临界区语句块】标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。lock语句确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入一段锁定代码,那么,在锁定对象被释放之前,它将一直处于等待状态。
实际上,lock语句完全等价于如下的Monitor类调用形式:
try
{
Monitor.Enter(【要锁定的引用类型对象】);
【临界区语句块】
}
finally
{
Monitor.Exit(【要锁定的引用类型对象】);
}
因此上述程序的Producer和Consumer方法也可以简写为:
// 生产者方法
public void Producer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
for (int j = 0; j < 16; ++j)
{
try
{
// 临界区
lock (synObj)
{
// 构造共享缓冲区
Random r = new Random();
int bufSize = r.Next() % 64;
char[] s = new char[bufSize];
for (int i = 0; i < bufSize; ++i)
{
s[i] = (char)((int)'A' + r.Next() % 26);
}
buffer = new string(s);
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
buffer);
// 通知消费者数据已经准备好
Monitor.Pulse(synObj);
// 休眠,将时间片让给消费者
Thread.Sleep(10);
}
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
break;
}
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
// 消费者方法
public void Consumer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
// 如果共享缓冲区为空,则休眠一秒,等待生产者线程构造缓冲区
if (buffer == null)
Thread.Sleep(1000);
for (int j = 0; j < 16; ++j)
{
try
{
// 临界区
lock (synObj)
{
// 打印共享缓冲区
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
buffer);
// 等待生产者的通知
Monitor.Wait(synObj, 1000);
}
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
break;
}
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
使用lock语句不仅简便,还可以避免一种很严重的错误。如果使用Monitor.Enter和Monitor.Exit时传入了一个值类型,编译器不能发现问题,程序能正确编译,但是程序将出现错误。而使用lock语句时,如果锁定的对象是值类型,就会出现编译错误。
19.3.3 使用Mutex
Mutex类是另一种常用的同步类。使用Mutex对象可以对资源进行独占访问。Mutex类比Monitor类使用更多的系统资源,但是它可以跨越应用程序域的边界进行封送处理,可用于控制多个等待的线程,并且可用于同步不同进程中的线程。
线程调用Mutex.WaitOne方法请求所有权。该调用会一直阻塞到Mutex可用为止,或直至达到指定的超时间隔。如果没有任何线程拥有它,则Mutex的状态为已发信号的状态。
线程通过调用Mutex.ReleaseMutex方法释放Mutex。Mutex是与线程关联的,即Mutex只能由拥有它的线程释放。如果线程释放不是它拥有的Mutex,则会在该线程中引发ApplicationException异常。
如果线程终止而未释放Mutex,则认为该Mutex已放弃。这是严重的编程错误,因为该Mutex正在保护的资源可能会处于不一致的状态。在.NET框架2.0中,获取该Mutex的下一个线程中会引发AbandonedMutexException异常。但是,在.NET框架1.0和1.1中不会引发异常,放弃的Mutex被设置为已发送信号状态,下一个等待线程将获得所有权。如果没有等待线程,则Mutex保持已发送信号状态。
Mutex分两种类型:本地Mutex和命名的系统Mutex。如果使用接受名称的构造函数创建了Mutex对象,那么该对象将与具有该名称的操作系统对象相关联。对于命名的系统Mutex,即使它们分别在不同的应用程序、进程或者内存空间中创建,只要它们的名称相同,在操作系统中都会指向同一个Mutex对象。本地Mutex仅存在于进程当中。进程中引用本地Mutex对象的任意线程都可以使用本地Mutex。
Mutex与Monitor存在两个很大的区别:
— Mutex可以用来同步属于不同应用程序或者进程的线程;而Monitor没有这个能力。
— 如果获得了Mutex的线程终止了,系统就会认为Mutex被自动释放,其他线程可以获得其控制权,而Monitor没有这种特征。
为了说明Mutex类的用法,我们将生产者和消费者线程分别放在两个应用程序中。在两个应用程序中都各自创建一个同名的Mutex对象,并利用它们来对生产者和消费者线程同步。
生产者线程所在的应用程序的代码如下:
// Mutex1.cs
// Mutex1示例
using System;
using System.IO;
using System.Threading;
using System.Diagnostics;
class Test
{
static void Main()
{
Test t = new Test();
// 进行测试
t.Go();
}
public void Go()
{
// 创建并启动线程
Thread t1 = new Thread(new ThreadStart(Producer));
t1.Name = "生产者线程";
t1.Start();
// 等待线程结束
t1.Join();
Console.WriteLine("按Enter键退出...");
Console.Read();
}
// 生产者方法
public void Producer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
// 创建互斥体
Mutex mutex = new Mutex(false, "CSharp_Mutex_test");
// 启动消费者进程
Process.Start("Mutex2.exe");
for (int j = 0; j < 16; ++j)
{
try
{
// 进入互斥体
mutex.WaitOne();
FileStream fs = new FileStream(@"d:\text.txt",
FileMode.OpenOrCreate, FileAccess.Write);
StreamWriter sw = new StreamWriter(fs);
// 构造字符串
Random r = new Random();
int bufSize = r.Next() % 64;
char[] s = new char[bufSize];
for (int i = 0; i < bufSize; ++i)
{
s[i] = (char)((int)'A' + r.Next() % 26);
}
string str = new string(s);
// 将字符串写入文件
sw.WriteLine(str);
sw.Close();
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name,
str);
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
break;
}
finally
{
// 退出互斥体
mutex.ReleaseMutex();
}
// 休眠,将时间片让给消费者
Thread.Sleep(1000);
}
// 关闭互斥体
mutex.Close();
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
}
消费者线程所在的应用程序的代码如下:
// Mutex2.cs
// Mutex2示例
using System;
using System.IO;
using System.Threading;
class Test
{
static void Main()
{
Test t = new Test();
// 进行测试
t.Go();
}
public void Go()
{
// 创建并启动线程
Thread t2 = new Thread(new ThreadStart(Consumer));
t2.Name = "消费者线程";
t2.Start();
// 等待线程结束
t2.Join();
Console.WriteLine("按Enter键退出...");
Console.Read();
}
// 消费者方法
public void Consumer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
// 创建互斥体
Mutex mutex = new Mutex(false, "CSharp_Mutex_test");
for (int j = 0; j < 16; ++j)
{
try
{
// 进入互斥体
mutex.WaitOne();
StreamReader sr = new StreamReader(@"d:\text.txt");
string s = sr.ReadLine();
sr.Close();
// 显示字符串的值
Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, s);
}
catch (System.Threading.ThreadInterruptedException)
{
Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
break;
}
finally
{
// 退出互斥体
mutex.ReleaseMutex();
}
// 休眠,将时间片让给消费者
Thread.Sleep(1000);
}
// 关闭互斥体
mutex.Close();
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
}
在【命令提示】窗口中启动Mutex1.exe,它会在另一个【命令提示】窗口中启动Mutex2.exe。可以看出,命名的系统Mutex将两个位于不同进程中的线程同步得非常好。
Mutex经常用于保护多个进程或者应用程序需要共享的资源,比如文件和内存等。
19.3.4 死锁和竞争条件
死锁是指使用共享资源的两个或多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能继续进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。
如果线程之间的同步出现问题,就会导致死锁。下面的程序演示一种典型的死锁的情形。线程一和线程二都需要锁对象obj1和obj2。线程一首先锁定了obj1,而线程二首先锁定了obj2。当线程一试图获取锁obj2时发现不能获取,于是开始等待。当线程二试图获取obj1时也发现不能获取,于是也开始等待。结果是线程一和线程二互相等待对方占有的共享资源,都不能继续执行下去。
// Deadlock.cs
// 死锁示例
using System;
using System.IO;
using System.Threading;
class Test
{
static object obj1 = new object();
static object obj2 = new object();
static void Main()
{
Console.WriteLine("创建线程");
// 创建线程
Thread thread1 = new Thread(new ThreadStart(ThreadProc1));
thread1.Name = "线程一";
Thread thread2 = new Thread(new ThreadStart(ThreadProc2));
thread2.Name = "线程二";
// 启动线程
thread1.Start();
thread2.Start();
// 等待两个线程结束
thread1.Join();
thread2.Join();
}
public static void ThreadProc1()
{
Console.WriteLine("{0}开始执行", Thread.CurrentThread.Name);
Console.WriteLine("{0}: 锁定obj1", Thread.CurrentThread.Name);
lock (obj1)
{
// 模拟一些工作
Thread.Sleep(1000);
Console.WriteLine("{0}: 等待obj2", Thread.CurrentThread.Name);
lock (obj2)
{
// 模拟一些工作
Thread.Sleep(1000);
}
}
Console.WriteLine("{0}结束", Thread.CurrentThread.Name);
}
public static void ThreadProc2()
{
Console.WriteLine("{0}开始执行", Thread.CurrentThread.Name);
Console.WriteLine("{0}: 锁定obj2", Thread.CurrentThread.Name);
lock (obj2)
{
// 模拟一些工作
Thread.Sleep(1000);
Console.WriteLine("{0}: 等待obj1", Thread.CurrentThread.Name);
lock (obj1)
{
// 模拟一些工作
Thread.Sleep(1000);
}
}
Console.WriteLine("{0}结束", Thread.CurrentThread.Name);
}
}
竞争条件是当程序的结果取决于两个或更多个线程中的哪一个先到达某一特定代码块时出现的一种缺陷。多次运行程序将产生不同的结果,而且给定的任何一次运行的结果都不可预知。
下面的程序演示了常见的生产者-消费者关系中,因为同步不当而引起的竞争条件问题。
// RaceConditions.cs
// 生产者消费者造成的竞争条件示例
using System;
using System.Threading;
class Test
{
private long counter = 0;
static void Main()
{
Test t = new Test();
// 进行测试
t.Go();
}
public void Go()
{
Thread t1 = new Thread(new ThreadStart(Producer));
t1.Name = "生产者线程";
t1.Start();
Thread t2 = new Thread(new ThreadStart(Consumer));
t2.Name = "消费者线程";
t2.Start();
// 等待两个线程结束
t1.Join();
t2.Join();
}
// 生产者方法
public void Producer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
while (counter < 1000)
{
long temp = counter;
// 模拟一些工作
Thread.Sleep(100);
lock (this)
{
temp++; // 加一
// 模拟一些工作
Thread.Sleep(10);
Monitor.Pulse(this);
}
// 给counter赋值,并显示结果
counter = temp;
Console.WriteLine("{0}:counter = {1}",
Thread.CurrentThread.Name, counter);
// 休眠,将时间片让给消费者
Thread.Sleep(100);
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
// 消费者方法
public void Consumer()
{
Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);
while (counter < 1000)
{
long temp;
lock (this)
{
temp = counter;
// 模拟一些工作
Thread.Sleep(10);
Monitor.Wait(this, 1000);
}
// 显示counter的值
Console.WriteLine("{0}:counter = {1}",
Thread.CurrentThread.Name, temp);
// 休眠,将时间片让给消费者
Thread.Sleep(100);
}
Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
}
}
上述程序的输出结果如下:
生产者线程:开始执行
消费者线程:开始执行
生产者线程:counter = 1
消费者线程:counter = 0
生产者线程:counter = 2
消费者线程:counter = 1
生产者线程:counter = 3
消费者线程:counter = 2
生产者线程:counter = 4
消费者线程:counter = 3
生产者线程:counter = 5
消费者线程:counter = 4
生产者线程:counter = 6
消费者线程:counter = 5
生产者线程:counter = 7
消费者线程:counter = 6
生产者线程:counter = 8
消费者线程:counter = 7
消费者线程:counter = 8
生产者线程:counter = 9
消费者线程:counter = 9
生产者线程:counter = 10
消费者线程:counter = 10
可以看出,消费者从生产者那里得到的数据有很多错误。与死锁相比,竞争条件更难发现,经常会造成很大的危害。