当多个线程请求锁定同一个对象时,CLR是否保证按照请求的顺序获取锁?
我写了一个测试,看看这是否属实,似乎表明是,但我不确定这是否是确定的。
class LockSequence
{
private static readonly object _lock = new object();
private static DateTime _dueTime;
public static void Test()
{
var states = new List<State>();
_dueTime = DateTime.Now.AddSeconds(5);
for (int i = 0; i < 10; i++)
{
var state = new State {Index = i};
ThreadPool.QueueUserWorkItem(Go, state);
states.Add(state);
Thread.Sleep(100);
}
states.ForEach(s => s.Sync.WaitOne());
states.ForEach(s => s.Sync.Close());
}
private static void Go(object state)
{
var s = (State) state;
Console.WriteLine("Go entered: " + s.Index);
lock (_lock)
{
Console.WriteLine("{0,2} got lock", s.Index);
if (_dueTime > DateTime.Now)
{
var time = _dueTime - DateTime.Now;
Console.WriteLine("{0,2} sleeping for {1} ticks", s.Index, time.Ticks);
Thread.Sleep(time);
}
Console.WriteLine("{0,2} exiting lock", s.Index);
}
s.Sync.Set();
}
private class State
{
public int Index;
public readonly ManualResetEvent Sync = new ManualResetEvent(false);
}
}
打印:
进入:0
0得到了锁定
0睡觉49979998蜱虫
进入:1
进入:2
进入:3
进入:4
进入:5
进入:6
进入:7
进入:8
进入:9
0退出锁
1获得锁定
1睡眠5001蜱
1退出锁
2得到了锁定
2睡觉5001蜱
2退出锁
3得到了锁定
3睡5001蜱
3退出锁
4得到了锁定
4睡5001蜱
4退出锁
5获得了锁定
5睡觉5001蜱
5退出锁
6得到了锁定
6退出锁
7获得了锁定
7退出锁
8获得了锁定
8退出锁定
9获得了锁定
9退出锁
答案 0 :(得分:73)
Wait
/ Notify
,但我也有一种潜在的怀疑也是为了锁定。
我绝对不会依赖它 - 如果您需要在序列中发生事情,请构建Queue<T>
或类似的东西。
因为监视器在内部使用内核对象,所以它们表现出与OS同步机制也表现出的相同的粗略FIFO行为(在前一章中描述)。监视器是不公平的,所以如果另一个线程试图在唤醒的等待线程尝试获取锁之前获取锁,则允许偷偷摸摸的线程获取锁。
“粗略FIFO”位是我之前想到的,“偷偷摸摸的线程”位进一步证明你不应该对FIFO排序做出假设。
答案 1 :(得分:10)
记录lock
语句使用Monitor
类来实现它的行为,而Monitor类的文档没有提及(我能找到)公平性。因此,您不应该依赖请求顺序获取请求的锁。
事实上,Jeffery Richter撰写的一篇文章实际上表明lock
不公平:
当然 - 这是一篇旧文章所以事情可能已经发生了变化,但鉴于Monitor
课程中关于公平的合同没有做出任何承诺,你需要承担最坏的情况。
答案 2 :(得分:10)
正常的CLR锁定不保证是FIFO。
但是,有一个QueuedLock class in this answer 可以提供有保障的FIFO锁定行为。
答案 3 :(得分:2)
与问题略有不同,但ThreadPool甚至不保证它将按添加顺序执行排队的工作项。如果需要顺序执行异步任务,一个选项是使用TPL任务(也通过Reactive Extensions向后移植到.NET 3.5)。它看起来像这样:
public static void Test()
{
var states = new List<State>();
_dueTime = DateTime.Now.AddSeconds(5);
var initialState = new State() { Index = 0 };
var initialTask = new Task(Go, initialState);
Task priorTask = initialTask;
for (int i = 1; i < 10; i++)
{
var state = new State { Index = i };
priorTask = priorTask.ContinueWith(t => Go(state));
states.Add(state);
Thread.Sleep(100);
}
Task finalTask = priorTask;
initialTask.Start();
finalTask.Wait();
}
这有一些优点:
保证执行顺序。
您不再需要显式锁定(TPL负责处理这些细节)。
您不再需要事件,也不再需要等待所有事件。您可以简单地说:等待上一个任务完成。
如果在任何任务中抛出异常,后续任务将被中止,并且调用Wait将重新抛出异常。这可能与您期望的行为相匹配,也可能不相符,但通常是顺序相关任务的最佳行为。
通过使用TPL,您可以为将来的扩展增加灵活性,例如取消支持,等待并行任务继续扩展等。
答案 4 :(得分:0)
我正在使用此方法进行FIFO锁定
public class QueuedActions
{
private readonly object _internalSyncronizer = new object();
private readonly ConcurrentQueue<Action> _actionsQueue = new ConcurrentQueue<Action>();
public void Execute(Action action)
{
// ReSharper disable once InconsistentlySynchronizedField
_actionsQueue.Enqueue(action);
lock (_internalSyncronizer)
{
Action nextAction;
if (_actionsQueue.TryDequeue(out nextAction))
{
nextAction.Invoke();
}
else
{
throw new Exception("Something is wrong. How come there is nothing in the queue?");
}
}
}
}
当线程在锁定中等待时,ConcurrentQueue将命令执行操作。