锁定C#

时间:2020-03-06 14:27:40  来源:igfitidea点击:

我还是不清楚什么时候应该在一些代码周围包装一个锁。我的一般经验法则是在读取或者写入静态变量时将操作包装在锁中。但是,当仅读取静态变量时(例如,它是在类型初始化期间设置的只读),则不需要将其包装在lock语句中,对吗?我最近看到了一些类似于以下示例的代码,这使我认为我的多线程知识可能存在一些不足:

class Foo
{
    private static readonly string bar = "O_o";

    private bool TrySomething()
    {
        string bar;

        lock(Foo.objectToLockOn)
        {
            bar = Foo.bar;          
        }       

        // Do something with bar
    }
}

但这对我来说毫无意义-为什么读寄存器会出现并发问题?

同样,此示例提出了另一个问题。其中之一比另一个更好吗? (例如,示例二持有锁的时间更少了?)我想我可以拆卸MSIL了……

class Foo
{
    private static string joke = "yo momma";

    private string GetJoke()
    {
        lock(Foo.objectToLockOn)
        {
            return Foo.joke;
        }
    }
}

class Foo
{
    private static string joke = "yo momma";

        private string GetJoke()
        {
            string joke;

            lock(Foo.objectToLockOn)
            {
                joke = Foo.joke;
            }

            return joke;
        }
}

解决方案

如果我们只是将值写入指针,则无需锁定,因为该操作是原子的。通常,我们应该在需要进行至少涉及两个原子操作(读或者写)的事务时锁定,该事务取决于状态在开始和结束之间不发生变化。

就是说,我来自Java领域,那里所有变量的读取和写入都是原子操作。这里的其他答案表明.NET是不同的。

脏读?

读取或者写入32位或者更小的字段是C#中的原子操作。据我所知,我们无需锁定所提供的代码。

我认为,我们应该尽力不要将静态变量放在需要从不同线程读取/写入静态变量的位置。在这种情况下,它们实际上是所有全局变量都可以自由使用的变量,而全局变量几乎总是一件坏事。

话虽如此,如果我们确实将静态变量放在这样的位置,则可能需要在读取期间锁定,以防万一,请记住,另一个线程可能在读取期间突然进入并更​​改了值,如果这样做,我们可能会导致数据损坏。除非我们通过锁定确保读取操作,否则不一定是原子操作。与写入相同,它们也不总是原子操作。

编辑:
正如Mark所指出的,对于Creads中的某些原语来说,它们总是原子的。但是请注意其他数据类型。

至于"哪个更好"的问题,它们是相同的,因为函数作用域未用于其他任何用途。

在我看来,在第一种情况下,不需要锁。确保使用静态初始化程序初始化bar是线程安全的。由于我们只读取过该值,因此无需锁定它。如果价值永远不会改变,就不会有任何争执,为什么要锁定呢?

由于我们编写的代码均未在初始化后修改静态字段,因此无需进行任何锁定。仅用新值替换字符串也不需要同步,除非新值取决于读取旧值的结果。

静态字段不是唯一需要同步的事物,任何可以修改的共享引用都容易受到同步问题的影响。

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        count++;
    }
}

我们可能假设执行TrySomething方法的两个线程会很好。但事实并非如此。

  • 线程A将计数(0)的值读入寄存器,以便可以递增。
  • 上下文切换!线程调度程序确定线程A有足够的执行时间。接下来的是线程B。
  • 线程B将计数(0)的值读入寄存器。
  • 线程B增加寄存器。
  • 线程B保存结果(1)进行计数。
  • 上下文切换回A。
  • 线程A将保存在其堆栈中的count(0)值重新加载到寄存器中。
  • 线程A增加寄存器。
  • 线程A保存结果(1)进行计数。

因此,即使我们两次调用count ++,count的值也才刚刚从0变为1. 让我们使代码成为线程安全的:

class Foo
{
    private int count = 0;
    private readonly object sync = new object();
    public void TrySomething()    
    {
        lock(sync)
            count++;
    }
}

现在,当线程A被中断时,线程B不会打乱计数,因为它将打到lock语句,然后阻塞直到线程A释放同步。

顺便说一句,还有另一种方法可以使递增的Int32和Int64成为线程安全的:

class Foo
{
    private int count = 0;
    public void TrySomething()    
    {
        System.Threading.Interlocked.Increment(ref count);
    }
}

关于问题的第二部分,我想我会选择比较容易理解的那个,任何性能差异都可以忽略不计。早期的优化是万恶之源,等等。

为什么穿线很难