Locks(或者Critical sections)
锁定是一种一次只允许一个线程进入特定代码区段的机制,通过加锁实现。被锁定的代码区段称为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关键字的例子在上面已经出现,我们可以用它来锁定某个对象。其实lock是Monitor
类的简写方式,下面将会提到。
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);
}
MethodImpl.Synchronized 属性
通过使用该属性,使得一个函数本身被同步。
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)。
有一点要注意的是,为了避免性能损失,锁定的区域要尽可能的小,尽管你可以锁定整个函数,但是如非必要,只锁定有同步需要的变量即可。