简体   繁体   English

C# 手动锁定/解锁

[英]C# manual lock/unlock

I have a function in C# that can be called multiple times from multiple threads and I want it to be done only once so I thought about this:我在 C# 中有一个 function 可以从多个线程中多次调用,我希望它只执行一次,所以我想到了这个:

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

The problem is _DoSomething takes a long time and I don't want many threads to wait on it when they can just see that done is true.问题是_DoSomething需要很长时间,我不希望很多线程在看到done是真的时等待它。
Something like this can be a workaround:这样的事情可能是一种解决方法:

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

But just doing the locking and unlocking manually will be much better.但是手动进行锁定和解锁会好得多。
How can I manually lock and unlock just like the lock(object) does?如何像lock(object)一样手动锁定和解锁? I need it to use same interface as lock so that this manual way and lock will block each other (for more complex cases).我需要它使用与lock相同的接口,以便这种手动方式和lock会相互阻塞(对于更复杂的情况)。

The lock keyword is just syntactic sugar for Monitor.Enter and Monitor.Exit: lock关键字只是 Monitor.Enter 和 Monitor.Exit 的语法糖:

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

is the same as是相同的

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

Thomas suggests double-checked locking in his answer.托马斯建议在他的答案中仔细检查锁定。 This is problematic.这是有问题的。 First off, you should not use low-lock techniques unless you have demonstrated that you have a real performance problem that is solved by the low-lock technique .首先,你不应该使用低锁技术,除非你已经证明你有一个真正的性能问题可以通过低锁技术解决 Low-lock techniques are insanely difficult to get right.低锁定技术非常难以正确使用。

Second, it is problematic because we don't know what "_DoSomething" does or what consequences of its actions we are going to rely on.其次,这是有问题的,因为我们不知道“_DoSomething”做了什么,也不知道我们将依赖它的行为的后果。

Third, as I pointed out in a comment above, it seems crazy to return that the _DoSomething is "done" when another thread is in fact still in the process of doing it.第三,正如我在上面的评论中指出的那样,当另一个线程实际上仍在执行此操作时,返回 _DoSomething 已“完成”似乎很疯狂。 I don't understand why you have that requirement, and I'm going to assume that it is a mistake.我不明白你为什么有这个要求,我会假设这是一个错误。 The problems with this pattern still exist even if we set "done" after "_DoSomething" does its thing.即使我们在“_DoSomething”完成它之后设置“done”,这种模式的问题仍然存在。

Consider the following:考虑以下:

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?
    }

Is this threadsafe in all possible implementations of C#?在 C# 的所有可能实现中,这个线程安全吗? I don't think it is.我不认为它是。 Remember, non-volatile reads may be moved around in time by the processor cache .请记住,处理器缓存可能会及时移动非易失性读取 The C# language guarantees that volatile reads are consistently ordered with respect to critical execution points like locks, and it guarantees that non-volatile reads are consistent within a single thread of execution, but it does not guarantee that non-volatile reads are consistent in any way across threads of execution. C# 语言保证易失性读取相对于锁等关键执行点的顺序一致,并且它保证非易失性读取在单个执行线程中是一致的,但它保证非易失性读取在任何线程中都是一致的跨执行线程的方式。

Let's look at an example.让我们看一个例子。

Suppose there are two threads, Alpha and Bravo.假设有两个线程,Alpha 和 Bravo。 Both call DoIt on a fresh instance of MyClass.两者都在 MyClass 的新实例上调用 DoIt。 What happens?怎么了?

On thread Bravo, the processor cache happens to do a (non-volatile,) fetch of the memory location for x.在线程 Bravo 上,处理器缓存碰巧对 x 的 memory 位置进行(非易失性)提取。 which contains zero.其中包含零。 "done" happens to be on a different page of memory which is not fetched into the cache quite yet. “完成”恰好位于 memory 的不同页面上,该页面尚未完全提取到缓存中。

On thread Alpha at the "same time" on a different processor DoIt calls DoSomething.在不同处理器 DoIt 上的“同时”线程 Alpha 上调用 DoSomething。 Thread Alpha now runs everything in there. Thread Alpha 现在运行其中的所有内容。 When thread Alpha is done its work, done is true and x is 123 on Alpha's processor.当线程 Alpha 完成其工作时,done 为真,并且 x 在 Alpha 的处理器上为 123。 Thread Alpha's processor flushes those facts back out to main memory. Thread Alpha 的处理器将这些事实刷新回主 memory。

Thread bravo now runs DoSomething. Thread bravo 现在运行 DoSomething。 It reads the page of main memory containing "done" into the processor cache and sees that it is true.它将包含“完成”的主 memory 页面读取到处理器缓存中,并看到它是真的。

So now "done" is true, but "x" is still zero in the processor cache for thread Bravo.所以现在“完成”是真的,但“x”在线程 Bravo 的处理器缓存中仍然为零。 Thread Bravo is not required to invalidate the portion of the cache that contains "x" being zero because on thread Bravo neither the read of "done" nor the read of "x" were volatile reads.线程 Bravo 不需要使包含“x”为零的缓存部分无效,因为在线程 Bravo 上,“done”的读取和“x”的读取都不是易失性读取。

The proposed version of double-checked locking is not actually double-checked locking at all.双重检查锁定的提议版本实际上根本不是双重检查锁定。 When you change the double-checked locking pattern you need to start over again from scratch and re-analyze everything .当您更改双重检查锁定模式时,您需要从头开始重新开始并重新分析所有内容

The way to make this version of the pattern correct is to make at least the first read of "done" into a volatile read.使这个版本的模式正确的方法是至少将“完成”的第一次读取变成易失性读取。 Then the read of "x" will not be permitted to move "ahead" of the volatile read to "done".那么“x”的读取将不允许移动到易失性读取的“前面”到“完成”。

You can check the value of done before and after the lock:你可以在锁之前之后检查done的值:

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

This way you won't enter the lock if done is true.这样,如果done为真,您将不会进入锁。 The second check inside the lock is to cope with race conditions if two threads enter the first if at the same time.锁内的第二个检查是为了应对竞争条件, if两个线程同时进入第一个。

BTW, you shouldn't lock on this , because it can cause deadlocks.顺便说一句,你不应该锁定this ,因为它会导致死锁。 Lock on a private field instead (like private readonly object _syncLock = new object() )改为锁定私有字段(如private readonly object _syncLock = new object()

The lock keyword is just syntactic sugar for the Monitor class. lock关键字只是Monitor class 的语法糖。 Also you could call Monitor.Enter() ,Monitor.Exit() .您也可以调用Monitor.Enter()Monitor.Exit()

But the Monitor class itself has also the functions TryEnter() and Wait() which could help in your situation.但是 Monitor class 本身也具有TryEnter()Wait()可以帮助您解决问题的功能。

I know this answer comes several years late, but none of the current answers seem to address your actual scenario, which only became apparent after your comment :我知道这个答案来晚了几年,但目前的答案似乎都没有解决你的实际情况,只有在你发表评论后才变得明显:

The other threads don't need to use any information generated by ReallyDoSomething.其他线程不需要使用由 RealDoSomething 生成的任何信息。

If the other threads don't need to wait for the operation to complete, the second code snippet in your question would work fine.如果其他线程不需要等待操作完成,那么您问题中的第二个代码片段就可以正常工作。 You can optimize it further by eliminating your lock entirely and using an atomic operation instead:您可以通过完全消除锁定并使用原子操作来进一步优化它:

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

Furthermore, if your _DoSomething() is a fire-and-forget operation, then you might not even need the first thread to wait for it, allowing it to run asynchronously in a task on the thread pool:此外,如果您的_DoSomething()是一个即发即弃的操作,那么您甚至可能不需要第一个线程来等待它,从而允许它在线程池上的任务中异步运行:

int done = 0;

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM