C# 双重检查锁定是否是确保任务调用一次的最干净选项?

C# 双重检查锁定是否是确保任务调用一次的最干净选项?,c#,multithreading,C#,Multithreading,我有一个类需要定期处理它的私有状态(ConcurrentDictionary)。我是通过Task.Factory.StartNew(doStuff)完成这项工作的。因为此任务正在修改集合,所以我希望确保当条件变为true时,doStuff只执行一次。它正在做的工作可以安全地忽略竞争条件,但我不希望冗余任务占用资源,并在可能的情况下挤出同步。现在,我通过双重检查锁定来控制它,它工作得很好,但在方法末尾附加的代码非常冗长: public void method() { ... //

我有一个类需要定期处理它的私有状态(ConcurrentDictionary)。我是通过
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);
}