在多线程代码中,多个线程可能会访问一些公共的资源(变量、方法逻辑等等),这些公共资源称为临界区(共享区);临界区的资源是不安全,所以需要通过线程同步对多个访问临界区的线程进行控制。

同样,有些时候我们需要多个线程按照特定的顺序执行,这时候,我们也需要进行线程同步。

下面,我们就看看C#中通过lock和Monitor进行线程同步。

lock关键字

lock是一种非常简单而且经常使用的线程同步方式,lock 关键字将语句块标记为临界区。 lock 确保当一个线程位于代码的临界区时,另一个线程不能进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待,直到该对象被释放。

下面看一个简单的例子:

namespace LockTest
{
class PrintNum
{
private object lockObj = new object(); public void PrintOddNum()
{
lock (lockObj)
{
Console.WriteLine("Print Odd numbers:");
for (int i = ; i < ; i++)
{
if(i% != )
Console.Write(i);
Thread.Sleep();
}
Console.WriteLine();
}
}
} class Program
{
static void Main(string[] args)
{
PrintNum printNum = new PrintNum();
for (int i = ; i < ; i++)
{
Thread temp = new Thread(new ThreadStart(printNum.PrintOddNum));
temp.Start();
} Console.Read();
}
}
}

这段代码比较容易理解,我们通过lock关键字把打印奇数的逻辑包在了临界区中,这样就可以保证同时只用一个线程执行临界区中的逻辑,代码打印如下:

使用lock的注意点

lock关键字的使用还是比较简单的,但是使用lock的时候还是有一些需要注意的地方。lock关键字可以锁住任何object类型及其派生类,但是尽量不要用public 类型的,否则实例将超出代码的控制范围。根据MSDN,常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:

  • 如果实例可以被公共访问,将出现 lock (this) 问题。
  • 如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
  • 由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock("myLock") 问题。

下面举个例子看看lock(this)的问题,假如我们把PrintOddNum中改成lock(this),并且在主线程中使用lock (printNum)。

namespace LockTest
{
class PrintNum
{
private object lockObj = new object(); public void PrintOddNum()
{
lock (this)
{
Console.WriteLine("Print Odd numbers:");
for (int i = ; i < ; i++)
{
if (i % != )
Console.Write(i);
Thread.Sleep();
}
Console.WriteLine();
}
}
} class Program
{
static void Main(string[] args)
{
PrintNum printNum = new PrintNum();
for (int i = ; i < ; i++)
{
Thread temp = new Thread(new ThreadStart(printNum.PrintOddNum));
temp.Start();
} lock (printNum)
{
Thread.Sleep();
Console.WriteLine("Main thread will delay 5 seconds");
} Console.Read();
}
}
}

代码的输出可能如下,因为Main函数和PrintNum类型中都对printNum对象进行了加锁,所以当主线程获得了互斥锁之后,其他子线程都被block住了,没有办法执行PrintOddNum方法了。

所以说,最好定义 private 对象 或 private static 对象进行上锁,从而保护所有实例所共有的数据。

lock的本质

lock关键字其实是一个语法糖,如果过查看IL代码,会发现lock 调用块开始位置为Monitor::Enter,块结束位置为Monitor::Exit。

为了保证Exit方法肯定会被调用,还专门用了一个try/finally语句块,这样即使代码出现了异常,也能保证Monitor::Exit能够被调用到。

.try
{
IL_0003: ldarg.0
IL_0004: ldfld object LockTest.PrintNum::lockObj
IL_0009: dup
IL_000a: stloc.2
IL_000b: ldloca.s '<>s__LockTaken0'
IL_000d: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
IL_0012: nop
IL_0013: nop
IL_0014: ldstr "Print Odd numbers:"
IL_0019: call void [mscorlib]System.Console::WriteLine(string)
……
IL_0052: leave.s IL_0064
} // end .try
finally
{
IL_0054: ldloc.1
IL_0055: ldc.i4.0
IL_0056: ceq
IL_0058: stloc.3
IL_0059: ldloc.3
IL_005a: brtrue.s IL_0063 IL_005c: ldloc.2
IL_005d: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0062: nop IL_0063: endfinally
} // end handler

那么下面我们就看看如何通过Monitor来进行线程同步。

Monitor类型

Monitor类通过互斥锁来进行对共享区的同步,当一个线程进入共享区时,会取得互斥锁的控制权,其他线程则必须等待。

前面了解到了,lock关键字就是一个语法糖,实际上lock使用的就是Monitor类型的Enter和Exit方法。很多情况下lock就可以满足需求了,但是当我们需要更进一步的线程同步时,就需要使用Monitor类型了。

下面看看Monitor类型的主要方法:

  • public static void Enter(object obj);
    • 在指定对象上获取互斥锁
  • public static void Exit(object obj);
    • 释放指定对象上的互斥锁
  • public static void Pulse(object obj);
    • 通知等待队列中的线程锁定对象状态的更改
  • public static bool TryEnter(object obj);
    • 试图获取指定对象的互斥锁,如果获得了互斥锁就返回true;否则返回false
    • TryEnter(Object, Int32)形式,表示在指定的毫秒数内尝试获取指定对象上的互斥锁
  • public static bool Wait(object obj);
    • 释放对象上的锁并阻止当前线程,直到它重新获取该锁

对于Enter和Exit,就不进行更多的介绍了,下面看看Pulse、Wait和TryEnter的使用。

Pulse和Wait

上面对Pulse和Wait方法的介绍还是很抽象的,下面进一步了解Pulse和Wait。

  • Wait:当线程调用 Wait 时,它释放对象的锁并进入等待队列。对象的就绪队列中的下一个线程(如果有)获取锁并拥有对对象的独占使用。所有调用 Wait 的线程都将留在等待队列中,直到它们接收到由锁的所有者发送的 Pulse 或 PulseAll 的信号为止。
  • Pulse:只有锁的当前所有者可以使用 Pulse 向等待对象发出信号。如果发送了 Pulse,则只影响位于等待队列最前面的线程。如果发送了 PulseAll,则将影响正等待该对象的所有线程。接收到信号后,一个或多个线程将离开等待队列而进入就绪队列。 在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁

使用注意事项:

  • 在使用Enter和Exit方法的时候,建议像lock的IL代码一样,使用try/finally语句块对Enter和Exit进行包装。
  • Pulse 、PulseAll 和 Wait 方法必须从同步的代码块内调用。
  • 在使用Pulse/Wait进行线程同步的时候,一定要牢记,Monitor 类不对指示 Pulse 方法已被调用的状态进行维护。 因此,如果在没有等待线程时调用 Pulse,则下一个调用 Wait 的线程将阻止,似乎 Pulse 从未被调用过。 如果两个线程正在使用 Pulse 和 Wait 交互,则可能导致死锁

下面看一个例子,模拟一个回合制的对打游戏,超人大战蜘蛛侠,通过Pulse/Wait,保证两人交替出招。

namespace MointorTest
{
class GamePlayer
{
public string PlayerName { get; set; }
public string EnemyName { get; set; }
} class Program
{
private static object monitorObj = new object();
private static int bloodAttack = ; static void Main(string[] args)
{
GamePlayer spiderMan = new GamePlayer { PlayerName = "Spider Man", EnemyName = "Super Man" };
Thread spiderManThread = new Thread(new ParameterizedThreadStart(GameAttack)); GamePlayer superMan = new GamePlayer { PlayerName = "Super Man", EnemyName = "Spider Man" };
Thread superManThread = new Thread(new ParameterizedThreadStart(GameAttack));
spiderManThread.Start(spiderMan);
superManThread.Start(superMan); spiderManThread.Join();
superManThread.Join();
Console.WriteLine("Game Over"); Console.Read();
} public static void GameAttack(object param)
{
GamePlayer gamePlayer = (GamePlayer)param; try
{
Monitor.Enter(monitorObj);
int blood = ;
Random ran = new Random();
while (blood > && bloodAttack >= )
{
blood -= bloodAttack;
if (blood > )
{
bloodAttack = ran.Next();
Console.WriteLine("{0}'s blood is {1}, attack {2} {3}", gamePlayer.PlayerName, blood, gamePlayer.EnemyName, bloodAttack);
}
else
{
Console.WriteLine("{0} is dead!!!", gamePlayer.PlayerName);
bloodAttack = -;
} Thread.Sleep();
Monitor.Pulse(monitorObj);
Monitor.Wait(monitorObj);
}
}
finally
{
Monitor.PulseAll(monitorObj);
Monitor.Exit(monitorObj);
}
}
}
}

代码的输出为下,注意在finally语句块中加入了"Monitor.PulseAll(monitorObj);",这样可以确保最后一次在等待队列中的线程可以顺利执行到最后。

TryEnter避免死等

当我们使用lock的时候,没有获得互斥锁的线程会一直等待,知道该线程获得互斥锁为止。这样就产生了线程死等的现象。

但是,在Monitor类型中,有了一个TryEnter(Object, Int32)方法,线程会尝试等待一段时间来获取互斥锁,如果超时仍未获得互斥锁,那么该方法就会返回false。

下面看一个例子:

namespace MointorTest
{
class Program
{
private static object monitorObj = new object(); static void Main(string[] args)
{
Thread firstThread = new Thread(new ThreadStart(TryEnterTest));
firstThread.Name = "firstThread";
Thread secondThread = new Thread(new ThreadStart(TryEnterTest));
secondThread.Name = "secondThread";
firstThread.Start();
secondThread.Start();
Console.Read(); } public static void TryEnterTest()
{
if (!Monitor.TryEnter(monitorObj, ))
{
Console.WriteLine("Thread {0} wait 5 seconds, didn't get the lock", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} completed!", Thread.CurrentThread.Name);
return;
}
try
{
Monitor.Enter(monitorObj);
Console.WriteLine("Thread {0} get the lock and will run 10 seconds", Thread.CurrentThread.Name);
Thread.Sleep();
Console.WriteLine("Thread {0} completed!", Thread.CurrentThread.Name);
}
finally
{
Monitor.Exit(monitorObj);
}
}
}
}

代码的输出为下,secondThread首先获得了互斥锁,并且会执行10秒钟;然后firstThread会等待5秒钟,仍然获取互斥锁失败。

为了对比演示,也可以把代码中"Thread.Sleep(10000);"换成"Thread.Sleep(2000);",这样就可以看到等待5秒钟,并且获取互斥锁成功的输出了。

例子:通过Monitor实现互斥Queue

为了进一步熟悉Monitor的使用,下面看一个互斥Queue的例子,producer和consumer可以通过多线程的方式访问互斥Queue。

namespace BlockingQueue
{
class BlockingQueue<T>
{
private object lockObj = new object();
public int QueueSize { get; set; }
private Queue<T> queue; public BlockingQueue()
{
this.queue = new Queue<T>(this.QueueSize);
} public bool EnQueue(T item)
{
lock (lockObj)
{
while (this.queue.Count() >= this.QueueSize)
{
Monitor.Wait(lockObj);
}
this.queue.Enqueue(item);
Console.WriteLine("---> 0000" + item.ToString());
Monitor.PulseAll(lockObj);
}
return true; } public bool DeQueue(out T item)
{
lock (lockObj)
{
while (this.queue.Count() == )
{
if (!Monitor.Wait(lockObj, ))
{
item = default(T);
return false;
};
}
item = this.queue.Dequeue();
Console.WriteLine("" + item + " <---");
Monitor.PulseAll(lockObj);
}
return true;
}
} class Program
{
static void Main(string[] args)
{
BlockingQueue<string> bQueue = new BlockingQueue<string>();
bQueue.QueueSize = ; Random ran = new Random(); //producer
new Thread(
() => {
for (int i = ; i < ; i++)
{
Thread.Sleep(ran.Next());
bQueue.EnQueue(i.ToString()); }
Console.WriteLine("producer quit!");
}).Start(); //producer
new Thread(
() =>
{
for (int i = ; i < ; i++)
{
Thread.Sleep(ran.Next());
bQueue.EnQueue(i.ToString()); }
Console.WriteLine("producer quit!");
}).Start(); //consumer
new Thread(
() =>
{
while (true)
{
Thread.Sleep(ran.Next());
string item = string.Empty;
if (!bQueue.DeQueue(out item))
{
break;
};
}
Console.WriteLine("consumer quit!");
}).Start(); Console.Read();
}
}
}

代码的输出为,例子中设置了BlockingQueue的size为3。

同时,在DeQueue方法中使用了"public static bool Wait(object obj, int millisecondsTimeout)"方法,这个方法将释放对象上的锁并阻止当前线程,直到它重新获取该锁;如果超过指定的超时间隔,则线程进入就绪队列。

总结

本文介绍了C#中如何通过lock和Monitor进行线程同步,如果仅仅是进行临界区的保护,那么我们可以简单的使用lock关键字,lock关键字是Monitor的一种语法糖。

所有lock能做的,Monitor都能做,Monitor能做的,lock不一定能做,Monitor提供了一些额外的功能:

  • 通过TryEnter(Object, Int32)方法可以设置一个超时时间,避免线程死等
  • 通过Monitor.Wait()和Monitor.Pulse(),可以进行更细致的线程同步控制

下一篇将介绍一下如何通过同步句柄(WaitHandle)来进行线程同步。

线程同步 – lock和Monitor的更多相关文章

  1. 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLock

    [源码下载] 重新想象 Windows 8 Store Apps (46) - 多线程之线程同步: Lock, Monitor, Interlocked, Mutex, ReaderWriterLoc ...

  2. 【转】多线程:C#线程同步lock,Monitor,Mutex,同步事件和等待句柄(上)

    本篇从Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的类关系图开始,希望通过 本篇的介绍能对常见的线程同步方法有一个整体的认识,而对 ...

  3. C#多线程:深入了解线程同步lock,Monitor,Mutex,同步事件和等待句柄(中)

    本篇继续介绍WaitHandler类及其子类 Mutex,ManualResetEvent,AutoResetEvent的用法..NET中线程同步的方式多的让人看了眼花缭乱,究竟该怎么去理解呢?其实, ...

  4. C#线程同步与死锁Monitor

    在上一讲介绍了使用lock来实现C#线程同步.实际上,这个lock是C#的一个障眼法,在C#编译器编译lock语句时,将其编译成了调用Monitor类.先看看下面的C#源代码: public stat ...

  5. 线程同步——lock锁

    线程同步即解决线程安全问题的第三种方式——使用lock锁 代码实现: 其中,ReentrantLock是lock接口的实现类,这边是使用多态创建,访问成员方法时,编译看左,运行看右: Reentran ...

  6. C# 线程同步之排它锁/Monitor监视器类

    一.Monitor类说明,提供同步访问对象的机制. 1.位于System.Threading命名空间下,mscorlib.dll程序集中. 2.Monitor通过获取和释放排它锁的方式实现多线程的同步 ...

  7. 线程同步 Lock接口

    同步:★★★★★ 好处:解决了线程安全问题. 弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁. 定义同步是有前提的: 1,必须要有两个或者两个以上的线程,才需要同步. 2,多个线程必须保证使用 ...

  8. 线程同步Lock锁

    Lock接口历史 java1.5版本之前只有synchronized一种锁,lock是java1.5版本之后提供的接口.lock接口与synchronized接口功能相同,但是需要手动获取锁和释放锁. ...

  9. 谈谈线程同步Lock和unLock

    Lock可以使用Condition进行线程之间的调度,它有更好的灵活性,而且在一个对象里面可以有多个Condition(即对象监视器),则线程可以注册在不同的Condition,从而可以 有选择性的调 ...

随机推荐

  1. Ubuntu下架设FTP服务器

    Linux下提供了很多的ftp服务器,这里我选用了安全,快速,简单的vsftpd作为FTP服务器.本文是我在自己的Ubuntu 10.10 -32 位系统下搭建的.搭建方法简单,按照本过程,您也可以完 ...

  2. PHP调用JAVA的WebService简单实例

    使用PHP调用JAVA语言开发的WebService.客户端提交两个String类型的参数,服务端返回一个对象类型.服务端使用AXIS-1.4作为SOAP引擎.客户端为PHP5.2.9,使用NuSOA ...

  3. JavaScript:属性的操作

    一.属性的设置和获取 1.属性的设置和获取主要有两种方式: <!DOCTYPE html> <html lang="en"> <head> &l ...

  4. 如何将mysql表结构导出成Excel格式的(并带备注)另附转为word表格的方法

    方法一: 1.使用一个MySQL管理工具:SQLyog,点击菜单栏“数据库”下拉的最后一项: 导出的格式如下: 2.要想转成Excel格式的只需手动将该表复制到Excel中去. 方法二: 1.以下用的 ...

  5. gitlab安装与配置(Centos6.8)

    0.Centos7请参照官方文档 https://about.gitlab.com/installation/#centos-7 1. Install and configure the necess ...

  6. android 开发 ANR

    记录一下: 问题出现原因:自定义加载对话框导致,查明是否有引用dialog的地方.

  7. e741. 将标签的焦点置于关联的文本框上面

    This example associates a label with a text field using setLabelFor(). A mnemonic is set on the labe ...

  8. Linq to Entity 动态拼接查询条件(重点是OR)

    public static class PredicateExtensions { /// <summary> /// 机关函数应用True时:单个AND有效,多个AND有效:单个OR无效 ...

  9. unity----------------3D模型讲解

    图文详解Unity3D中Material的Tiling和Offset是怎么回事 回到顶部(go to top) Tiling和Offset概述 Tiling表示UV坐标的缩放倍数,Offset表示UV ...

  10. Git -- 新增分支添加新功能

    软件开发中,总有无穷无尽的新的功能要不断添加进来. 添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合 ...