线程同步是指并发线程高效、有序的访问共享资源所采用的技术,所谓同步,是指某一时刻只有一个线程可以访问资源,只有当资源所有者主自动放弃了代码或资源的所有权时,其他线程才可以使用这些资源。
线程同步可以分别使用C#中的lock关键字、Monitor类、Interlocked类和Mutex类实现,下面对这4种实现方法进行详细介绍。
1.使用C#中的lock关键字实现线程同步
lock关键字可以用来确保代码块完成运行,而不会被其他线程中断,它是通过在代码块运行期间为给定对象获取互斥锁来实现的。
lock语句以关键字lock开头,它有一个作为参数的对象,在该参数的后面还有一个一次只能由一个线程执行的代码块。Lock语句语法格式如下。
Object thisLock = new Object();
lock (thisLock)
{
//要运行的代码块
}
提供给lock语句的参数必须为基于引用类型的对象,该对象用来定义锁的范围。严格来说,提供给lock语句的参数只是用来唯一标识由多个线程共享的资源,所以它可以是任意类实例,然而,实际上,此参数通常表示需要进行线程同步的资源。例如,如果一个容器对象将被多个线程使用,则可以将该容器传递给lock语句,而lock语句中的代码块将访问该容器。只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。
通常,最好避免锁定public类型或不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则lock(this)可能会有问题,因为不受控制的代码也可能会锁定该对象,这将可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题,锁定字符串尤其危险,因为字符串被公共语言运行库(CLR)“暂留”,这意味着整个程序中任何给定字符串都只有一个实例,因此,只要在应用程序进程中的任何具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例,因此,最好锁定不会被暂留的私有或受保护成员。
说明:事实上lock语句是用Monitor类来实现的,它等效于try/finally语句块,使用lock关键字通常比直接使用Monitor类更可取,一方面是因为lock更简洁,另一方面是因为lock确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过finally关键字来实现的,无论是否引发异常它都执行关联的代码块。
例如创建一个控制台应用程序,其中自定义了一个LockThread方法,该方法中使用lock关键字锁定当前线程,然后在Main方法中通过Program的类对象调用LockThread自定义方法。代码如下。
static void Main(string[] args)
{
Program myProgram = new Program();//实例化类对象
myProgram.LockThread();//调用锁定线程方法
}
void LockThread()
{
lock (this) //锁定当前线程,以实现同步
{
Console.WriteLine("锁定线程以实现线程同步");
}
}
2.使用Monitor类实现线程同步
Monitor类提供了同步对对象的访问机制,它通过向单个线程授予对象锁来控制对对象的访问,对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象锁时,其他任何线程都不能获取该锁。
Monitor类的主要功能如下。
它根据需要与某个对象相关联。
它是未绑定的,也就是说可以直接从任何上下文调用它。
不能创建Monitor类的实例。
Monitor类的常用方法及说明如表1所示。
表1 Monitor类的常用方法及说明
方法 | 说明 |
Enter | 在指定对象上获取排他锁 |
Exit | 释放指定对象上的排他锁 |
Pulse | 通知等待队列中的线程锁定对象状态的更改 |
PulseAll | 通知所有的等待线程对象状态的更改 |
TryEnter | 试图获取指定对象的排他锁 |
Wait | 释放对象上的锁并阻止当前线程,直到它重新获取该锁 |
注意:使用Monitor类锁定的是对象(即引用类型)而不是值类型。
例 创建一个控制台应用程序,其中自定义了一个LockThread方法,该方法中首先使用Monitor类的Enter方法锁定当前线程,然后再调用Monitor类的Exit方法释放当前线程,最后在Main方法中通过Program的类对象调用LockThread自定义方法。代码如下。
static void Main(string[] args)
{
Program myProgram = new Program();//实例化类对象
myProgram.LockThread();//调用锁定线程方法
}
void LockThread()
{
Monitor.Enter(this); //锁定当前线程
Console.WriteLine("锁定线程以实现线程同步");
Monitor.Exit(this); //释放当前线程
}
3.使用Mutex类实现线程同步
当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex类是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。Mutex类与监视器类似,它防止多个线程在某一时间同时执行某个代码块,然而与监视器不同的是,Mutex类可以用来使跨进程的线程同步。
可以使用WaitHandle.WaitOne方法请求互斥体的所属权,拥有互斥体的线程可以在对WaitOne方法的重复调用中请求相同的互斥体而不会阻止其执行,但线程必须调用同样多次数的ReleaseMutex方法以释放互斥体的所属权。Mutex类强制线程标识,因此互斥体只能由获得它的线程释放。
当用于进程间同步时,Mutex称为“命名Mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个Mutex对象。
Mutex类的常用方法及说明如表1所示。
表1 Mutex类的常用方法及说明
方法 | 说明 |
Close | 在派生类中被重写时,释放由当前WaitHandle持有的所有资源 |
OpenExisting | 打开现有的已命名互斥体 |
ReleaseMutex | 释放Mutex一次 |
SignalAndWait | 原子操作的形式,向一个WaitHandle发出信号并等待另一个 |
WaitAll | 等待指定数组中的所有元素都收到信号 |
WaitAny | 等待指定数组中的任一元素收到信号 |
WaitOne | 当在派生类中重写时,阻止当前线程,直到当前的WaitHandle收到信号 |
使用Mutex类实现线程同步很简单,首先实例化一个Mutex类对象,它的构造函数中比较常用的有public Mutex(bool initallyOwned),其中,参数initallyOwned指定了创建该对象的线程是否希望立即获得其所有权,当在一个资源得到保护的类中创建Mutex类对象时,常将该参数设置为false;然后在需要单线程访问的地方调用其等待方法,等待方法请求Mutex对象的所有权,这时,如果该所有权被另一个线程所拥有,则阻塞请求线程,并将其放入等待队列中,请求线程将保持阻塞,直到Mutex对象收到了其所有者线程发出将其释放的信号为止。所有者线程在终止时释放Mutex对象,或者调用ReleaseMutex方法来释放Mutex对象。
说明:尽管Mutex类可以用于进程内的线程同步,但是使用Monitor类通常更为可取,因为Monitor监视器是专门为.NET Framework而设计的,因而它可以更好地利用资源。相比之下,Mutex类是Win32构造的包装。尽管Mutex类比监视器更为强大,但是相对于Monitor类,它所需要的互操作转换更消耗计算资源。
例如创建一个控制台应用程序,其中自定义了一个LockThread方法,该方法中首先使用Mutex类对象的WaitOne方法阻止当前线程,然后再调用Mutex类对象的ReleaseMutex方法释放Mutex对象,即释放当前线程,最后在Main方法中通过Program的类对象调用LockThread自定义方法。代码如下。
static void Main(string[] args)
{
Program myProgram = new Program();//实例化类对象
myProgram.LockThread();//调用锁定线程方法
}
void LockThread()
{
Mutex myMutex=new Mutex(false); //实例化Mutex类对象
myMutex.WaitOne();//阻止当前线程
Console.WriteLine("锁定线程以实现线程同步");
myMutex.ReleaseMutex();//释放Mutex对象
}