C#.NET中的线程同步类(一)

本文主要描述的是线程同步类之间的区别,其它线程同步的相关内容:C#.NET中的线程同步类(二)C#.NET中的同步策略

Monitor

关于这个类,个人觉得有点迷糊。对于这个类有“对对象进行同步锁操作”和“对代码段进行同步锁操作”两种说法,在我的代码验证中得出的结论是:并没有能够在多线程环境下锁定同步对象(待进一步验证)。所以,在此仅引用我认为对的定义,定义引用自《C#线程参考手册》一书。

首先来看Monitor的功能定义:Monitor用于同步代码区,其方式是使用Monitor.Enter()方法获得一个锁,然后使用Monitor.Exit()方法释放该锁。锁的概念通常用于解释Monitor类。一个线程获得锁,期他线程就要等到该锁被释放后才能使用。一旦在代码区上获取了一个锁,就可以在Monitor.Enter()和Monitor.Exit()程序块内使用如下方法:

Wait() -- 此方法用于释放对象上的锁,并暂停当前线程,直到它重新获得锁。

Pulse() -- 此方法用于通知正在队列中等待的线程,对象的状态已经改变了。

PulseAll() -- 此方法用于通知所有正在队列中等待的线程,对象的状态已经有了改变。

Monitor方法是静态方法,能被Monitor类自身调用,而不是由该类的实例调用。在.NET Framework中,每个对象都有一个与之相关的锁,可获取和释放该锁,这样,在任一时刻仅有一个线程可以访问对象的实例变量和方法。与之类似,.NET Framework中的每个对象也提供一种允许它处于等待状态的机制。正如锁的机制,这种机制的主要原因是帮助线程间的通信。如果一个线程进入对象的重要代码段,并需要一定的条件才能存在,而另一线程可以在该代码段中创建该条件,此时就需要这种机制。

Enter()和Exit()方法使用如下:

object newObj = new object();

Monitor.Enter(newObj); //锁定对象
//此时只有当前线程能操作newObj对象
Monitor.Exit(newObj); //释放锁

而通常情况下会使用下面的代码,来保证被锁对象最终会被释放

object newObj = new object();

if (Monitor.TryEnter(newObj)) //试图获得对象的独占锁
{
	//try finally机制可以保证Monitor锁定的对象无论是否产生异常都能被释放
	//而不至于使对象一直被锁定 产生死锁的危险
	try
	{
		//处理代码
	}
	finally
	{
		Monitor.Exit(newObj);
	}
}
else
{
	//其它处理
}

Monitor类的Wait()/Pulse()两个静态方法,Wait()和Pulse()机制用于线程间的交互。当在一个对象上执行Wait()时,正在访问该对象的线程就会进入等待状态,直到它得到一个唤醒的信号。Pulse()和PulseAll()用于给等待线程发送信号。由于网上这两个方法的示例较多,这里就不具体分析了。以下示例仅验证Monitor是同步了对象还是同步代码区,仅为个人浅见。

1、声明一个含有一个公共成员的类

class NewObject
{
	public int Count = 0;

	public void ChangeValueMonitor()
	{
		Monitor.Enter(this);
		Thread.Sleep(3000);
		Count++;
		Console.WriteLine(Thread.CurrentThread.Name + " : Count Value is: " + Count);
		Thread.Sleep(5000);
		Monitor.Exit(this);
	}
}

2、声明一个包含被锁对象及线程代码的类

class TestThreadManager
{
	private NewObject m_Obj = null;

	public TestThreadManager(NewObject obj)
	{
		m_Obj = obj;
	}

	public void FunctionMonitor()
	{
		Console.WriteLine("Monitor Enter...");
		m_Obj.ChangeValueMonitor();
		Console.WriteLine("Monitor Exit...");
	}

	public void FunctionChangeValue()
	{
		Console.WriteLine("Change Value Function is running...");

		m_Obj.Count++;
		Console.WriteLine(Thread.CurrentThread.Name + " : Count Value is: " + m_Obj.Count);

		Console.WriteLine("Change Value Function Exit...");
	}
}

其中FunctionMonitor()方法也可写成如下,效果是一样的:

	public void FunctionMonitor()
	{
		Console.WriteLine("Monitor Enter...");
		Monitor.Enter(m_Obj);
		Thread.Sleep(3000);
		m_Obj.Count++;
		Console.WriteLine(Thread.CurrentThread.Name + " : Count Value is: " + m_Obj.Count);
		Thread.Sleep(5000);
		Monitor.Exit(m_Obj);
		Console.WriteLine("Monitor Exit...");
	}

3、创建两个竞争执行锁对象修改的线程

static void Main(string[] args)
{
	NewObject newObj = new NewObject();

	TestThreadManager manager = new TestThreadManager(newObj);

	Thread t1 = new Thread(new ThreadStart(manager.FunctionMonitor));
	t1.Name = "Monitor";
	Thread t2 = new Thread(new ThreadStart(manager.FunctionChangeValue));
	t2.Name = "Changer";
	
	//显示newObj.Count被改变前的值
	Console.WriteLine("Thread Main : Count Value is: " + newObj.Count);
	t1.Start();
	Thread.Sleep(1000); //用于保证线程t1进入锁后线程t2才开始执行
	t2.Start();

	Console.ReadLine();
}

4、执行结果如下:

Thread Main : Count Value is: 0
Monitor : Enter...
Change Value Function is running...
Changer : Count Value is: 1
Change Value Function Exit...
Monitor : Count Value is: 2
Monitor : Exit...

可见,Monitor并没有锁定对象newObj使其不被其它线程操作。

而当把Main中的代码修改如下:

static void Main(string[] args)
{
	NewObject newObj = new NewObject();

	TestThreadManager manager = new TestThreadManager(newObj);

	Thread t1 = new Thread(new ThreadStart(manager.FunctionMonitor));
	t1.Name = "Monitor";
	Thread t2 = new Thread(new ThreadStart(manager.FunctionMonitor));
	t2.Name = "Changer";
	
	//显示newObj.Count被改变前的值
	Console.WriteLine("Thread Main : Count Value is: " + newObj.Count);
	t1.Start();
	Thread.Sleep(1000); //用于保证线程t1进入锁后线程t2才开始执行
	t2.Start();

	Console.ReadLine();
}

得到的结果与lock()方法相同,详细效果见以下lock()方法的分析。可见,此处Monitor是将Enter()与Exit()之间的代码段同步了。

Lock

此方法是对Monitor方法的封装,与Monitor不同的是Lock方法只是对“{}”内的代码段进行同步锁定,简单且形象地说就是:这段代码是个临界资源,同时只能有一个线程能执行这段代码。以下用代码来说明。

1、声明一个含有一个公共成员的类

class NewObject
{
	public int Count = 0;
}

2、声明一个包含被锁对象及线程代码的类

class TestThreadManager
{
	private NewObject m_Obj = null;

	public TestThreadManager(NewObject obj)
	{
		m_Obj = obj;
	}

	public void FunctionLock()
	{
		Console.WriteLine(Thread.CurrentThread.Name + " : Enter...");

		//此处也可以是lock除值类型的其它对象 如:Lock(this)
		//但需要注意的是lock仅仅是后面的{}内的代码段 其它线程对m_Obj还有操作权
		lock (m_Obj) 
		{
			Thread.Sleep(3000);
			m_Obj.Count++;
			Console.WriteLine(Thread.CurrentThread.Name + " : Count Value is: " + m_Obj.Count);
			Thread.Sleep(5000);
		}

		Console.WriteLine(Thread.CurrentThread.Name + " : Exit...");
	}
}

3、创建两个竞争执行代码段的线程

static void Main(string[] args)
{
	NewObject newObj = new NewObject();

	TestThreadManager manager = new TestThreadManager(newObj);

	Thread t1 = new Thread(new ThreadStart(manager.FunctionLock));
	t1.Name = "Locker";
	Thread t2 = new Thread(new ThreadStart(manager.FunctionLock));
	t2.Name = "Changer";
	
	//显示newObj.Count被改变前的值
	Console.WriteLine("Thread Main : Count Value is: " + newObj.Count);
	t1.Start();
	Thread.Sleep(1000); //用于保证线程t1进入锁后线程t2才开始执行
	t2.Start();

	Console.ReadLine();
}

4、执行结果如下:

Thread Main : Count Value is: 0
Locker : Enter...
Changer : Enter...
Locker : Count Value is: 1
Locker : Exit...
Changer : Count Value is: 2
Changer : Exit...

由以上结果可见,当第一个线程Locker开始执行,使用lock(m_Obj)方法锁住后面的代码段时,虽然线程Changer也进入了函数体,但由于函数体内的代码段已被其它线程锁定,于是Changer没能继续执行,而是挂起等待其它的线程执行完,释放该段代码。

当Locker线程执行完并释放锁,Changer才得以执行该段代码并将Count值加1。

而关于lock方法是否锁住了m_Obj对象,我们可以修改代码做个验证。

1、向TestThreadManager类中添加成员方法

	//用与在其它线程锁定代码或者m_Obj对象时尝试修改m_Obj对象
	public void FunctionChangeValue()
	{
		Console.WriteLine("Change Value Function is running...");

		m_Obj.Count++;
		Console.WriteLine(Thread.CurrentThread.Name + " : Count Value is: " + m_Obj.Count);

		Console.WriteLine("Change Value Function Exit...");
	}

2、Main函数中修改如下

static void Main(string[] args)
{
	NewObject newObj = new NewObject();

	TestThreadManager manager = new TestThreadManager(newObj);

	Thread t1 = new Thread(new ThreadStart(manager.FunctionLock));
	t1.Name = "Locker";
	Thread t2 = new Thread(new ThreadStart(manager.FunctionChangeValue));
	t2.Name = "Changer";
	
	//显示newObj.Count被改变前的值
	Console.WriteLine("Thread Main : Count Value is: " + newObj.Count);
	t1.Start();
	Thread.Sleep(1000); //用于保证线程t1进入锁后线程t2才开始执行
	t2.Start();

	Console.ReadLine();
}

3、执行结果

Thread Main : Count Value is: 0
Locker : Enter...
Change Value Function is running...
Changer : Count Value is: 1
Change Value Function Exit...
Locker : Count Value is: 2
Locker : Exit...

结果显示很明显,虽然Locker线程中用lock方法锁住了同一个m_Obj对象,但在Locker线程释放锁前,Changer线程还是可以修改m_Obj.Count值。所以,lock()方法的作用仅限于同步代码而不是对象。

Mutex

先引用自MSDN的定义:当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex 是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。此外Mutex 类强制线程标识,因此互斥体只能由获得它的线程释放。

Mutex类似于Monitor/lock,不过不同的是Mutex要实例化,且锁定的是自己。其示例代码也类似,这里就不写出来了,上面的代码修改下就可以用来 演示Mutex。

Mutex类又分局部和全局的Mutex,由于在.NET中Mutex是对内核对象mutex的封装,且需要更大的开销而执行较慢,如果要在一个应用程序域内同步的话使用Monitor/lock更好。而Mutex类更适合用于进程同步,平时我用Mutex同步进程的情况不多,所以这里也没什么经验介绍。Mutex还有个很常见的用途:用于控制一个应用程序只能有一个实例在运行。示例代码很简单如下:

private static Mutex myMutex = null;

static void Main(string[] args)
{
	bool isCreated;

	//此处的作用是在第二个程序的实例试图再次生成以"ThisIsSingleInstance"为名的Mutex对象时
	//第三输出参数的值会指出是否系统中已有一个同名的Mutex对象 
	//第三个参数输出为true表示这是一个新的Mutex false表示已有一个同名的Mutex对象
	myMutex = new Mutex(true, "ThisIsSingleInstance", out isCreated);

	try
	{
		if (!isCreated)
		{
			Console.WriteLine("Program Instance is already exist. Press any key to exit.");
			Console.ReadKey();
			return;
		}
		else
		{
			Console.WriteLine("Instance is running...");
			return;
		}
	}
	finally //此处的try finally是为了保证Mutex能正确地释放
	{
		if (isCreated)
		{
			myMutex.ReleaseMutex();
		}

		myMutex.Close();
	}
}

现在才算了解了些Mutex的跨进程应用。

常用的三个同步类先记录到这,之后会补充其它几个同步类的介绍。

阅读更多
个人分类: 读书笔记
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭