为了理解同步访问共享资源,我们来看一个例子:2 个线程向 Dictionary 添加数据。
internal class Program
{
private static Int32 PRODUCT_NUMBER = 100;
private static Dictionary<Int32, String> dics = new Dictionary<Int32, String>();
static void Main(string[] args)
{
var tasks = new List<Task>();
for (int i = 0; i < 2; i++)
{
tasks.Add(Task.Run(() => { Produce(); }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine("main method finished.");
Console.ReadKey();
}
private static void Produce()
{
Random random = new Random();
for(int i = 0;i < PRODUCT_NUMBER; i++)
{
if (!dics.ContainsKey(i))
{
dics.Add(i, i.ToString());
Thread.Sleep(random.Next(50));
}
}
}
}
上面的代码出现了竞态条件,会出现类似于下面的错误:
System.ArgumentException
HResult=0x80070057
Message=An item with the same key has already been added. Key: 5
Source=System.Private.CoreLib
StackTrace:
at System.ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException[T](T key)
at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
at MonitorSample.Program.Produce() in D:\continental\adas_am_toolchain\src\conti.adas.am.toolchain\MonitorSample\Program.cs:line 30
at MonitorSample.Program.<>c.<Main>b__2_0() in D:\continental\adas_am_toolchain\src\conti.adas.am.toolchain\MonitorSample\Program.cs:line 15
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
为了防止竞态条件需要对来自多个线程的数据进行同步访问。
Monitor 类可以通过调用 Monitor.Enter, Monitor.TryEnter, 以及 Monitor.Exit 方法来获取和释放对象锁,从而对代码区域进行同步访问。
internal class Program
{
private static Int32 PRODUCT_NUMBER = 100;
private static Dictionary<Int32, String> dics = new Dictionary<Int32, String>();
private static Object objLocker = new Object();
static void Main(string[] args)
{
var tasks = new List<Task>();
for (int i = 0; i < 2; i++)
{
tasks.Add(Task.Run(() => { Produce(); }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine("main method finished.");
Console.ReadKey();
}
private static void Produce()
{
Random random = new Random();
for(int i = 0;i < PRODUCT_NUMBER; i++)
{
if (!dics.ContainsKey(i))
{
Monitor.Enter(objLocker);
if (!dics.ContainsKey(i))
{
dics.Add(i, i.ToString());
}
Monitor.Exit(objLocker);
Thread.Sleep(random.Next(50));
}
}
}
}
使用 lock 关键字也可以达到同步访问共享资源的目的,它的语法也相当简单:
internal class Program
{
private static Int32 PRODUCT_NUMBER = 100;
private static Dictionary<Int32, String> dics = new Dictionary<Int32, String>();
private static Object objLocker = new Object();
static void Main(string[] args)
{
var tasks = new List<Task>();
for (int i = 0; i < 2; i++)
{
tasks.Add(Task.Run(() => { Produce(); }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine("main method finished.");
Console.ReadKey();
}
private static void Produce()
{
Random random = new Random();
for(int i = 0;i < PRODUCT_NUMBER; i++)
{
if (!dics.ContainsKey(i))
{
lock (objLocker)
{
if (!dics.ContainsKey(i))
{
dics.Add(i, i.ToString());
}
}
Thread.Sleep(random.Next(50));
}
}
}
}
事实上,lock 语句的存在只是为了方便,就像 C# 的 using 语句一样。
lock(objLocker)
{
// your code
}
会被 C# 编译器翻译成等效于下面的代码块
Boolean lockAcquired = false;
try
{
Monitor.Enter(objLocker, ref lockAcquired);
// your code
}
finally
{
if (lockAcquired)
{
Monitor.Exit(objLocker);
}
}
如果仅仅是为了对代码块的同步访问,直接使用 lock 关键字即可,无需自己再去封装 Monitor.Enter 和 Monitor.Exit 方法。
由于 lock 语句是 Monitor 类中 Enter/Exit 方法的语法糖,因此 lock 还可以与 Monitor 类中的 Monitor.Wait 和 Monitor.Pulse 方法一起使用,以实现更高级的线程协调或通信。