锁定是一种一次只允许一个线程进入特定代码区段的机制,通过加锁实现。被锁定的代码区段称为critical section(关键区域)。锁定一段代码的方式有多种,下面将一一介绍。在介绍前,我们先来看看什么情况需要锁定:
using System; using System.Threading; namespace NoLockTest { /// <summary> /// This program is not thread safe /// as it could be accessed by 2 different /// simultaneous threads. As one thread could /// be set item2 to 0, just at the point that /// another thread was doing the division, /// leading to a DivideByZeroException /// </summary> class Program { static int item1=54, item2=21; public static Thread T1; public static Thread T2; static void Main(string[] args) { T1 = new Thread((ThreadStart)delegate { DoCalc(); }); T2 = new Thread((ThreadStart)delegate { DoCalc(); }); T1.Name = "T1"; T2.Name = "T2"; T1.Start(); T2.Start(); Console.ReadLine(); } private static void DoCalc() { item2 = 10; if (item1 != 0) Console.WriteLine(item1 / item2); item2 = 0; } } }
上面的代码不是线程安全的,因为它可能同时被两个线程操作,一个线程可能将item2
置为零,而此时如果另一线程做除法,则会导致DivideByZeroException
。尽管这种问题出现的几率比较小,难于调试发现,但这正是多线程问题复杂所在。因此,为避免这种问题的出现,我们需要采取安全措施。我们可以通过lock来解决这个问题:
using System; using System.Threading; namespace LockTest { /// <summary> /// This shows how to create a critical section /// using the lock keyword /// </summary> class Program { static object syncLock = new object(); static int item1 = 54, item2 = 21; public static Thread T1; public static Thread T2; static void Main(string[] args) { T1 = new Thread((ThreadStart)delegate { DoCalc(); }); T2 = new Thread((ThreadStart)delegate { DoCalc(); }); T1.Name = "T1"; T2.Name = "T2"; T1.Start(); T2.Start(); Console.ReadLine(); } private static void DoCalc() { lock (syncLock) { item2 = 10; if (item1 != 0) Console.WriteLine(item1 / item2); item2 = 0; } } } }
在这个例子中,lock关键字所包含的区域即为关键区域(critical sections)。每次只有一个线程可以锁定同步对象(syncLock
)。任何其它的竞争线程都将被阻塞,直到syncLock
被释放。等待线程处于一种队列状态,先道者可以优先得到服务。
有些读者可能会用lock(this)
或者lock(typeof(MyClass))
来作为同步对象,这是一个坏习惯,因为这些变量都是公开的,所以外部实体也有可能用它们来同步,从而对你的线程造成干扰,因此最好使用私有的同步对象。
下面简要介绍一下加锁的几种方式:
lock关键字的例子在上面已经出现,我们可以用它来锁定某个对象。其实lock是Monitor
类的简写方式,下面将会提到。
Monitor类可以达到lock关键字同样的效果。示例如下:
using System; using System.Threading; namespace LockTest { /// <summary> /// This shows how to create a critical section /// using the Monitor class /// </summary> class Program { static object syncLock = new object(); static int item1 = 54, item2 = 21; static void Main(string[] args) { Monitor.Enter(syncLock); try { if (item1 != 0) Console.WriteLine(item1 / item2); item2 = 0; } finally { Monitor.Exit(syncLock); } Console.ReadLine(); } } }
可以看出lock其实只是下面代码的语法糖而已:
Monitor.Enter(syncLock); try { } finally { Monitor.Exit(syncLock); }
通过使用该属性,使得一个函数本身被同步。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MethodImplSynchronizedTest { /// <summary> /// This shows how to create a critical section /// using the System.Runtime.CompilerServices.MethodImplAttribute /// </summary> class Program { static int item1=54, item2=21; static void Main(string[] args) { //make a call to different method //as cant Synchronize Main method DoWork(); } [System.Runtime.CompilerServices.MethodImpl (System.Runtime.CompilerServices.MethodImplOptions.Synchronized)] private static void DoWork() { if (item1 != 0) Console.WriteLine(item1 / item2); item2 = 0; Console.ReadLine(); } } }
上述例子说明了如何通过 System.Runtime.CompilerServices.MethodImplAttribute
来使一个函数同步(critical section)。
有一点要注意的是,为了避免性能损失,锁定的区域要尽可能的小,尽管你可以锁定整个函数,但是如非必要,只锁定有同步需要的变量即可。