C#中的锁定

Posted

技术标签:

【中文标题】C#中的锁定【英文标题】:Locking in C# 【发布时间】:2010-09-11 10:28:38 【问题描述】:

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

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;
        

【问题讨论】:

【参考方案1】:

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

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

class Foo

    private int count = 0;
    public void TrySomething()    
    
        count++;
    

您可能认为执行 TrySomething 方法的两个线程会很好。但事实并非如此。

    线程 A 将 count (0) 的值读入寄存器,以便递增。 上下文切换!线程调度程序决定线程 A 有足够的执行时间。接下来是线程 B。 线程 B 将 count (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 释放同步。

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

class Foo

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

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

Why threading is hard

【讨论】:

“早期优化是万恶之源等”。我认为这是“过早的优化”,如果在测量之后进行早期优化,那么早期优化是好的,过早的......是另一回事,通常根本没有测量...... 我认为“早期”在引用的上下文中意味着“过早”。 这里有点迷糊,我以为每个线程都有自己的自己的栈空间。由于 count 是一个 int (堆栈变量),所以这个值肯定会被隔离到一个单独的线程,在它自己的堆栈上? @miguel: 如果 count 是一个局部变量,那么你是对的,它只会存在于堆栈中,每个线程都有自己的 count,不会有线程安全问题。但是,count 是一个字段,因此不只存在于堆栈中,并且多个线程可以访问它。【参考方案2】:

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

【讨论】:

实际上这只是因为您使用的是 32 位操作系统 grin 如果您使用的是 Windows Mobile,我不太确定是不是这样。 然而,增加或增加一个 32 位的值并不是原子的,所以你还是要小心。 Matt,如果您要递增或添加到 32 位值,那将是一个写入操作,因此语句“读取或写入 32 位...字段是原子操作" 来自 Mark 是不正确的,不是吗?你的说法似乎与马克相矛盾。 递增一个值(除非你使用 AtomicIncrement)是读取 THEN 写入一个值。读取或写入都不会失败或被中断,但在这种情况下您可能会遇到竞争条件,此时可能需要锁定。【参考方案3】:

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

【讨论】:

【参考方案4】:

脏读?

【讨论】:

什么是“脏读”?这与不安全/非托管代码有关吗? 不,看看***的文章:en.wikipedia.org/wiki/…【参考方案5】:

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

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

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

【讨论】:

你会建议什么而不是使用静态变量?例如,我有一个 Databases.dll 程序集,它从 .config 文件中读取连接字符串,并将其存储在静态类中,以便可以使用它来创建 DataContext 对象。例如,有什么替代方案? 然而,在示例中,没有其他线程可以在读取期间更改值,因此不会争用该字段。所以没有锁,对吧? 是的,在那种情况下我会说没有锁......嗯。在这种情况下,您可能对静态变量没问题,实际上我可能也会这样做,因为在这种情况下(恕我直言)做其他事情是多余的。我会说根据具体情况处理静态变量。【参考方案6】:

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

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

【讨论】:

【参考方案7】:

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

【讨论】:

第二个示例的 IL 更短,但意义不大。你是对的。

以上是关于C#中的锁定的主要内容,如果未能解决你的问题,请参考以下文章

C# - 锁定和 StreamWrite 问题(多线程)

和 C# 中的两个位图

从存储在 MYSQL 中的 C# 生成唯一 ID

多线程(基础篇3)

为啥在双重检查锁定中使用 Volatile.Write?

在 asp.net 中锁定缓存的最佳方法是啥?