C#手动锁定/解锁

Dan*_*ani 22 c# multithreading locking

我在C#中有一个函数,可以从多个线程多次调用,我希望它只能完成一次,所以我想到了这个:

class MyClass
{
    bool done = false;
    public void DoSomething()
    {
        lock(this)
            if(!done)
            {
                done = true;
                _DoSomething();
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

问题是_DoSomething需要很长时间,我不希望很多线程等待它,只要他们可以看到这done是真的.
这样的事情可以解决方法:

class MyClass
{
    bool done = false;
    public void DoSomething()
    {
        bool doIt = false;
        lock(this)
            if(!done)
                doIt = done = true;
        if(doIt)
             _DoSomething();
    }
}
Run Code Online (Sandbox Code Playgroud)

但只需手动锁定和解锁就会好得多.
我怎样才能手动锁定和解锁lock(object)?我需要它使用相同的接口,lock因此这种手动方式lock将相互阻塞(对于更复杂的情况).

PVi*_*itt 30

lock关键字仅仅是Monitor.Enter和Monitor.Exit语法糖:

Monitor.Enter(o);
try
{
    //put your code here
}
finally
{
    Monitor.Exit(o);
}
Run Code Online (Sandbox Code Playgroud)

是相同的

lock(o)
{
    //put your code here
}
Run Code Online (Sandbox Code Playgroud)

  • 请注意,在C#4中,我们稍微更改了codegen,以便输入在*try*内部*.有关凌乱的详细信息,请参阅http://blogs.msdn.com/b/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx. (17认同)
  • 编辑使代码不正确。假设在进入 try 之后但在获取锁之前抛出一个线程中止异常。finally 块将运行并释放一个从未被占用的锁。 (2认同)

Eri*_*ert 28

托马斯建议双重检查锁定他的答案.这是有问题的.首先,你不应该使用低锁技术,除非你已经证明你有一个真正的性能问题,这是通过低锁技术解决的.低锁定技术很难做到正确.

其次,这是有问题的,因为我们不知道"_DoSomething"会做什么,或者我们将依赖它的行为的后果.

第三,正如我在上面的评论中指出的那样,当另一个线程实际上仍处于执行过程中时,返回_DoSomething已"完成"似乎很疯狂.我不明白为什么你有这个要求,我会认为这是一个错误.即使我们在"_DoSomething"之后设置"完成",这种模式的问题仍然存在.

考虑以下:

class MyClass 
{
     readonly object locker = new object();
     bool done = false;     
     public void DoSomething()     
     {         
        if (!done)
        {
            lock(locker)
            {
                if(!done)
                {
                    ReallyDoSomething();
                    done = true;
                }
            }
        }
    }

    int x;
    void ReallyDoSomething()
    {
        x = 123;
    }

    void DoIt()
    {
        DoSomething();
        int y = x;
        Debug.Assert(y == 123); // Can this fire?
    }
Run Code Online (Sandbox Code Playgroud)

这在C#的所有可能实现中都是线程安全的吗? 我认为不是.请记住,处理器缓存可能会及时移动非易失性读取.C#语言保证挥发性的读取,但始终对于像锁临界执行点预订的,它保证了非易失性读取都执行一个线程中保持一致,但它并不能保证非易失性读取在任何一致跨越执行线程的方式.

我们来看一个例子.

假设有两个线程,Alpha和Bravo.两者都在MyClass的新实例上调用DoIt.怎么了?

在线程Bravo上,处理器高速缓存碰巧执行(非易失性!)获取x的内存位置,其中包含零."完成"恰好位于不同的内存页面上,而目前尚未将其提取到缓存中.

在线程Alpha在"同一时间"在不同的处理器上DoIt调用DoSomething.线程Alpha现在运行在那里的一切.当线程Alpha完成其工作时,完成为真,并且Alpha在Alpha处理器上为123.Thread Alpha的处理器将这些事实刷回主存储器.

Thread bravo现在运行DoSomething.它将包含"done"的主内存页面读入处理器缓存,并看到它是真的.

所以现在"完成"是正确的,但"x"在线程Bravo的处理器缓存中仍为零.Thread Bravo不需要使包含"x"的缓存部分无效,因为在线程Bravo上,"完成"和"x"的读取都不是易失性读取.

建议的双重检查锁定版本实际上并不是双重检查锁定.当您更改双重检查的锁定模式时,您需要从头开始重新开始并重新分析所有内容.

使这个版本的模式正确的方法是至少将"完成"的第一次读取变为易失性读取.然后,不允许读取"x"将易失性读取"提前"移动到"完成".

  • @Thomas:我建议**除非你的名字是Joe Duffy**否则永远不要使用双重检查锁定.如果您确实想要危险地生活并发明自己的双重检查锁定版本,那么您可能不想使用Thread.VolatileRead,因为它可能具有不良的性能特征 - 在某些实现中我相信它会创建一个完整的内存栅栏,虽然我可能会记错了.您可能希望简单地使"完成"变为易失性,即使这会使其一些读取和写入不必要地变化. (6认同)