C# 双重检查锁定是否是确保任务调用一次的最干净选项?
我有一个类需要定期处理它的私有状态(ConcurrentDictionary)。我是通过C# 双重检查锁定是否是确保任务调用一次的最干净选项?,c#,multithreading,C#,Multithreading,我有一个类需要定期处理它的私有状态(ConcurrentDictionary)。我是通过Task.Factory.StartNew(doStuff)完成这项工作的。因为此任务正在修改集合,所以我希望确保当条件变为true时,doStuff只执行一次。它正在做的工作可以安全地忽略竞争条件,但我不希望冗余任务占用资源,并在可能的情况下挤出同步。现在,我通过双重检查锁定来控制它,它工作得很好,但在方法末尾附加的代码非常冗长: public void method() { ... //
Task.Factory.StartNew(doStuff)
完成这项工作的。因为此任务正在修改集合,所以我希望确保当条件变为true时,doStuff只执行一次。它正在做的工作可以安全地忽略竞争条件,但我不希望冗余任务占用资源,并在可能的情况下挤出同步。现在,我通过双重检查锁定来控制它,它工作得很好,但在方法末尾附加的代码非常冗长:
public void method()
{
...
// do the method related stuff. Upon executing this method, the condition for
// executing the task could become true, so it is appropriate to try to start it,
// even though it is not directly related to the semantics of this method
...
// now see if the task should be run
if (clock.Now > timestamp.Plus(interval) && periodicTask == null)
{
lock (lockObject)
{
if (periodicTask == null)
periodicTask = Task.Factory.StartNew(this.DoStuff);
}
}
}
private void DoStuff()
{
doActualProcessing();
//signal that task is complete
timestamp = clock.Now;
periodicTask = null;
}
这个设置可以防止DoStuff被额外调用,但它看起来太冗长,有点脏。理想情况下,我希望封装此单次调用逻辑,以便可以执行以下操作:
public void method()
{
// do the method related stuff.
...
// now see if the task should be run
startTaskExactlyOnce(DoStuff);
}
我看到的唯一选择可能是使用Lazy
来实现这一点,但在这里似乎不合适,因为:
1.我不是在初始化一个对象,我在运行一个任务
2.任务将定期运行,每个周期运行一次
3.我只需要启动任务,因此使用
Lazy.Value
导致执行时不会进行转换如果只需要一个线程修改字典,为什么要使用并发实现?在我看来,你只是需要一个计时器。或者您可以在处理方法的末尾安排下一次运行。如果您只需要一个线程修改字典,为什么要使用并发实现?在我看来,你只是需要一个计时器。或者,您可以将下一次运行安排在处理方法的末尾。通过与结合使用,您可以避免许多讨厌的(难以验证的)逻辑
Lazy周期;
void rundostuff without并发()
{
惰性周期ICTASKLAZYLOCAL=
新的Lazy(()=>Task.Factory.StartNew(this.DoStuff));
联锁比较交换(
参考日期:Asklazy,
在当地,
无效);
任务实际任务=periodicTaskLazy.Value;
}
void DoStuff()
{
//....
联锁交换(参考周期ICTASKLAZY,空);
}
尽管我怀疑这对于此类任务仲裁来说可能是一个更好的选择。通过结合使用,您可以避免许多更糟糕(且难以验证)的逻辑
Lazy周期;
void rundostuff without并发()
{
惰性周期ICTASKLAZYLOCAL=
新的Lazy(()=>Task.Factory.StartNew(this.DoStuff));
联锁比较交换(
参考日期:Asklazy,
在当地,
无效);
任务实际任务=periodicTaskLazy.Value;
}
void DoStuff()
{
//....
联锁交换(参考周期ICTASKLAZY,空);
}
尽管我怀疑这对于此类任务仲裁来说可能是一个更好的选择。就我个人而言,我会把赌注押在整个双重检查锁定的事情上。我认为它不会以你愿意支付的风险溢价为你买任何实质性的东西。尽管如此,您的具体实现可能会正常工作。但是,这主要是偶然的。继续读下去 首先,将立即发布
DoStuff
中的写入periodicTask
,因为1)x86硬件上的写入已经具有易失性语义,2)在特殊同步点为您生成隐式内存屏障,即在这种情况下,任务的结束,因此,即使在较弱的内存模型环境中,写端也可能是可以的
其次,在方法
中读取periodicTask
可能会通过任何优化,因为clock.Now
(假设其工作方式类似于DateTime.Now
)可能至少会在尝试从系统提取当前时间时生成一个获取栅栏屏障
注意到我一直在用“可能”这个词吗?在得出上述结论时,我做了几个假设。可能是我大错特错了。不过,我可以自信地说,我的假设很可能是真的。关键是为什么要冒险。即使我是对的,当下一个程序员进来时,这段代码可能很容易被破坏,因为他不理解你的意图,所以开始切换
让我们回到双重检查锁定模式开始的原因。正如我所说的,这真的不值得。当periodicTask
不是null
时,您希望方法
多久运行一次?在你获得任何有意义的收益之前,这种情况必须大大超过所有其他情况(通过…我不知道…可能是100000比1或其他)。一个锁的成本非常小,所以无论你玩什么把戏,都必须有一个真正的大爆炸,才能让你的努力值得。我只是不认为在你的场景中会发生这种情况。就我个人而言,我会把赌注押在整个双重检查锁定的事情上。我认为它不会以你愿意支付的风险溢价为你买任何实质性的东西。尽管如此,您的具体实现可能会正常工作。但是,这主要是偶然的。继续读下去
首先,将立即发布DoStuff
中的写入periodicTask
,因为1)x86硬件上的写入已经具有易失性语义,2)在特殊同步点为您生成隐式内存屏障,即在这种情况下,任务的结束,因此,即使在较弱的内存模型环境中,写端也可能是可以的
其次,在方法
中读取periodicTask
可能会通过任何优化,因为clock.Now
(假设其工作方式类似于DateTime.Now
)可能至少会在尝试提取当前时间f时生成一个获取栅栏屏障
Lazy<Task> periodicTaskLazy;
void RunDoStuffWithoutConcurrency()
{
Lazy<Task> periodicTaskLazyLocal =
new Lazy<Task>(() => Task.Factory.StartNew(this.DoStuff));
Interlocked.CompareExchange<Lazy<Task>>(
ref periodicTaskLazy,
periodicTaskLazyLocal,
null);
Task actualTask = periodicTaskLazy.Value;
}
void DoStuff()
{
//....
Interlocked.Exchange(ref periodicTaskLazy,null);
}