上一节主要讲了创建调用有参(多参)函数的线程和线程池的一些内容,这一节主要讲线程的同步。
多线程的出现解决了吞吐量和响应速度的问题,但同时也带来了资源共享问题,如死锁和资源争用。在为单个资源分配多个线程可能会导致同步问题。何为线程同步呢?所谓同步,是指多个线程之间存在先后执行顺序的关联关系。如果一个线程必须在另一个线程完成某个工作后才能继续执行,则必须考虑如何让其他保持同步,以确保在系统上同时运行多个线程而不会出现死锁或逻辑错误。
下面先看一个例子:
class Program
{
static void Main(string[] args)
{
Thread thread = new Thread(new ThreadStart(TestShow));
Thread thread1 = new Thread(new ThreadStart(TestShow1));
thread.Start();
thread1.Start();
Console.ReadKey();
}
static bool biaoJi = false;
static void TestShow()
{
if (!biaoJi)
{
biaoJi = true;
Console.WriteLine("标记为False");
}
}
static void TestShow1()
{
if (biaoJi)
{ Console.WriteLine("标记为True");}
}
这个程序很简单,就是创建两个线程,分别调用两个函数输出一句话,两个函数都共用到了一个全局变量biaoJi,在TestShow()里边将biaoJi的值改为False,运行之后的结果如下:
或者
出现第一种结果原因,我们在TestShow里将biaoJi值改为true,在TestShow1里执行if(biaoJi)时为biaoJi值为true,所以可以输出两句话。
出现第二句话的原因在于,两个线程同时访问biaoJi值,此时biaoJi值为发生变化,仍未false,故在执行TestShow1时biaoJi值仍为false,所以只能输出一句话
这里就出现了线程的同步问题,结果一是我们想要的结果,但是在程序的运行过程中,往往会出现像结果二那样的结果。
如果解决这样的问题呢?为了解决这些问题,System.Threading命名空间提供了多个用于同步线程的类。这些类包括Mutex,Monitor,Interlocked,AutoResetEvent. 下面会逐步介绍一些解决同步的方法。
一、最简单也是最常用的方法,C#提供的Lock方法
Lock关键字能确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码段,则它将一直等待,直到锁定的对象被释放以后才能进入临界区。
下面演示下如何使用Lock:
代码如下:
Private static readonly object obj=new object();
static void TestShow()
{
Lock(obj)
{
if (!biaoJi)
{
biaoJi = true;
Console.WriteLine("标记为False");
}
}
}
这样不管怎么调用显示的结果永远都是上例所示的第一种结果。
Lock使用方法:
Lock关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。 此语句的形式如下:
Object obj = new Object();lock (obj){ // code section.}
Lock注意事项:
最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据,锁定的对象不能为空。
二、Monitor类
Lock语句经过编译器解析为Monitor类。Monitor类的效果和Lock基本是一样的。
可以通过以下方式实现同步:
Lock方式: lock(obj)
{
//code section
}
Monitor类(为静态类): Monitor.Enter(obj);
//code section
//........
Monitor.Exit(obj);
示例:
static void TestShow()
{
Monitor.Enter(obj);
if (!biaoJi)
{
biaoJi = true;
Console.WriteLine("标记为False");
}
Monitor.Exit(obj);
}
除此之外Monitor还有一个优点就是可以设置一个等待获得锁定的超时值,用以避免无限期的锁定。通过Monitor.TryEnter(object obj,Timespan time)来设置等待获得锁定的最长时间
使用示例:
if(Monitor.TryEnter(obj,1000)){try { ........... }finally { //当时间超过1秒的时候,线程不再等待 Monitor.Exit(obj); }实例:
static void TestShow()
{
if (Monitor.TryEnter(obj, 2000))
{
try
{
if (!biaoJi)
{
Thread.Sleep(3000);
biaoJi = true;
Console.WriteLine("标记为False");
}
}
catch { Monitor.Exit(obj); } }
}
这样因为线程在修改biaoJi值时,休眠的3秒,超出了Monitor设置的等待时间,所以另一个线程已经开始执行了,执行时在函数TestShow1()中,biaoJi的值仍为false所以只输出了一句话。等到2个线程都执行完毕时,此时biaoJi的值为True。
三、Mutex
Mutex的功能和C# 中的Lock一样,不同的是它可以跨进程。在操作系统中,许多线程常常需要共享资源,而这些资源往往要求一次只能为一个线程服务,这种排他性地使用共享资源称为线程间的互斥。线程互斥实质上也是同步,可以看做一种特殊的线程同步。但是,进入和释放一个Mutex要花费几毫秒,效率会比较低。
通常我们会使用一个Mutex的实例,调用WaitOne方法来获取锁,ReleaseMutex方法来释放锁。
方法如下:
Mutex m = new Mutex();
m.WaitOne();
//code section
//...
m.ReleaseMutex();
此外我们还可以为WaitOne()函数设置参数,以防止无限期的等待。
Mutex类还有一些其他的方法:比如:
Mutex的WaitAll()函数//等待所有的线程操作
Mutex的WaitAny()函数//多个操作时,等待指定的某个线程操作
但是在操作结束后,一定要分别进行释放。
今天到这里基本上就这些了,当然这些都是比较常用的解决线程同步的方法,还有其他的一些方法比如AutoResetEvent 、ManualResetEvent 、 EventWaitHandle ,还有一个volatile关键字的同步方法,但是它只能在变量一级做同步。关于后边的上边所列的同步方法不常用,所以就不再做介绍了,有兴趣的朋友可以自己去了解下。希望可以对大家有所帮助。下一节会讲一下异步操作。