C#手动锁定/解锁

时间:2011-04-28 12:08:58

标签: c# multithreading locking

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

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

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

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

但手动锁定和解锁会更好 如何像lock(object)一样手动锁定和解锁?我需要它使用与lock相同的界面,以便这种手动方式和lock将相互阻止(对于更复杂的情况)。

5 个答案:

答案 0 :(得分:29)

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

Monitor.Enter(o);
try
{
    //put your code here
}
finally
{
    Monitor.Exit(o);
}

相同
lock(o)
{
    //put your code here
}

答案 1 :(得分:26)

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

其次,这是有问题的,因为我们不知道“_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?
    }

C#的所有可能实现中都有线程安全吗?我认为不是。请记住,非易失性读取可能会被处理器缓存及时移动。 C#语言保证了易失性读取与锁等关键执行点一致排序,并且它保证非易失性读取在单个执行线程中是一致的,但它确实保证非 - 易失性读取在任何执行线程中都是一致的。

让我们看一个例子。

假设有两个线程,Alpha和Bravo。两者都在MyClass的新实例上调用DoIt。会发生什么?

在线程Bravo上,处理器缓存恰好对x的内存位置进行(非易失性!)获取,其中包含零。 “完成”恰好位于不同的内存页面上,但尚未将其提取到缓存中。

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

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

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

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

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

答案 2 :(得分:4)

您可以在锁定后检查之前的done的值:

    if (!done)
    {
        lock(this)
        {
            if(!done)
            {
                done = true;
                _DoSomething();
            }
        }
    }

这样,如果done为真,您就不会进入锁定状态。如果两个线程同时进入第一个if,则锁内的第二个检查是应对竞争条件。

BTW,you shouldn't lock on this,因为它可能导致死锁。锁定私人字段(如private readonly object _syncLock = new object()

答案 3 :(得分:2)

lock关键字只是Monitor类的语法糖。您也可以拨打Monitor.Enter()Monitor.Exit()

但是Monitor类本身也有函数TryEnter()Wait(),它们可以帮助您处理。

答案 4 :(得分:1)

我知道这个答案会迟到几年,但目前的答案似乎都没有解决你的实际情况,只有在你comment之后才明白:

  

其他主题不需要使用ReallyDoSomething生成的任何信息。

如果其他线程不需要等待操作完成,则问题中的第二个代码段可以正常工作。您可以通过完全取消锁定并使用原子操作来进一步优化它:

private int done = 0;
public void DoSomething()
{
    if (Interlocked.Exchange(ref done, 1) == 0)   // only evaluates to true ONCE
        _DoSomething();
}

此外,如果您的_DoSomething()是一个即发即弃操作,那么您甚至可能不需要第一个线程等待它,允许它在线程池上的任务中异步运行:

int done = 0;

public void DoSomething()
{
    if (Interlocked.Exchange(ref done, 1) == 0)
        Task.Factory.StartNew(_DoSomething);
}