14

我仍然有点不清楚什么时候在一些代码周围加锁。我的一般经验法则是在读取或写入静态变量时将操作包装在锁中。但是当一个静态变量只被读取时(例如,它是在类型初始化期间设置的只读),访问它不需要包含在锁定语句中,对吗?我最近看到一些类似于以下示例的代码,这让我觉得我的多线程知识可能存在一些差距:

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;
        }
}
4

7 回答 7

23

由于您编写的代码在初始化后都没有修改静态字段,因此不需要任何锁定。只需用新值替换字符串也不需要同步,除非新值取决于读取旧值的结果。

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

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

您可能会认为执行 TrySomething 方法的两个线程会很好。但它不是。

  1. 线程 A 将 count (0) 的值读入寄存器,以便递增。
  2. 上下文切换!线程调度程序决定线程 A 有足够的执行时间。下一行是线程 B。
  3. 线程 B 将 count (0) 的值读入寄存器。
  4. 线程 B 递增寄存器。
  5. 线程 B 将结果 (1) 保存到计数。
  6. 上下文切换回 A。
  7. 线程 A 使用保存在其堆栈中的 count (0) 值重新加载寄存器。
  8. 线程 A 递增寄存器。
  9. 线程 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 释放同步。

顺便说一句,有另一种方法可以使递增 Int32s 和 Int64s 线程安全:

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

Regarding the second part of your question, I think I would just go with whichever is easier to read, any performance difference there will be negligible. Early optimisation is the root of all evil, etc.

Why threading is hard

于 2008-09-19T21:10:36.110 回答
8

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

于 2008-09-19T20:24:58.683 回答
3

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

于 2008-09-19T20:26:42.423 回答
1

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

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

于 2008-09-19T20:21:04.420 回答
1

脏读?

于 2008-09-19T20:22:20.143 回答
1

在我看来,您应该非常努力地不要将静态变量放在需要从不同线程读取/写入它们的位置。在这种情况下,它们本质上是免费的全局变量,而全局变量几乎总是一件坏事。

话虽如此,如果您确实将静态变量放在这样的位置,您可能希望在读取期间锁定,以防万一 - 请记住,另一个线程可能在读取期间突然介入并更改了值,如果确实如此,您最终可能会收到损坏的数据。读取不一定是原子操作,除非您通过锁定确保它们。与写入相同 - 它们也不总是原子操作。

编辑:正如 Mark 指出的,对于 C# 中的某些原语读取总是原子的。但要小心其他数据类型。

于 2008-09-19T20:24:59.980 回答
0

至于您的“哪个更好”的问题,它们是相同的,因为函数范围不用于其他任何事情。

于 2008-09-19T20:25:03.473 回答