本文主要描述的是线程同步类之间的区别,其它线程同步的相关内容: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的跨进程应用。
常用的三个同步类先记录到这,之后会补充其它几个同步类的介绍。
本文对比分析了C#.NET中的线程同步类Monitor、Lock和Mutex的功能、用法及其应用场景,通过示例代码展示了它们在多线程环境下的同步机制,特别是Monitor与Lock的区别在于Monitor同时锁定对象和代码区,而Lock仅锁定代码段;Mutex则用于实现进程间或应用程序域间的互斥访问。此外,文章还介绍了Mutex在控制应用程序实例唯一性的应用。
191

被折叠的 条评论
为什么被折叠?



