线程:锁定在引擎盖下

Hun*_*ter 2 c# multithreading locking

当我们使用锁定对象时会发生什么?我知道它运行时使用了monitor.Enter和Exit方法.但真正发生在引擎盖下的是什么?为什么只有用于锁定的引用类型?即使用于完成锁定的对象发生了变化,为什么它还能提供线程安全性呢?

在当前样本中,我们正在修改用于锁定目的的对象.理想情况下,这不是一种优选的方法,最佳实践是使用专用的私有范围变量.

static List<string> stringList = new List<string>();
    static void AddItems(object o)
    {
        for (int i = 0; i < 100; i++)
        {
            lock (stringList)
            {
                Thread.Sleep(20);
                stringList.Add(string.Format("Thread-{0},No-{1}", Thread.CurrentThread.ManagedThreadId, i));
            }
        }
        string[] listArray = null;

        lock(stringList)
        listArray = stringList.ToArray();

        foreach (string s in listArray)
        {
            Console.WriteLine(s);
        }
    }
Run Code Online (Sandbox Code Playgroud)

Tim*_*mwi 6

引擎盖下发生的事情是这样的:

  • 想象一下,该object类型中有一个隐藏字段.
  • Monitor.Enter()并Monitor.Exit()使用该字段相互通信.
  • 每个引用类型都继承该字段object.

Of course, the type of that field is something special: It’s a synchronization lock that works in a thread-safe manner. In reality, of course, it is not really a field in the CLR sense, but a special feature of the CLR that uses a chunk of memory within each object’s memory to implement that synchronization lock. (The exact implementation is described in "Safe Thread Synchronization" in the MSDN Magazine.)

为什么它仍然提供线程安全?我想你的意思是:为什么它不会破坏线程安全的对象的线程安全性?答案很简单:因为你可以拥有部分线程安全且部分不安全的对象.你可以拥有一个带有两个方法的对象,并且使用其中一个方法是线程安全的,而另一个方法则不是.Monitor.Enter()无论对象的其余部分是什么,都是线程安全的.

为什么只有用于锁定的引用类型?因为只有引用类型实际上在它们的内存块中具有这种特殊的魔力.值类型实际上只是值本身:在int的情况下是32位整数; 在自定义结构的情况下所有字段的串联.你可以传递一个值类型Monitor.Enter(),它不会抱怨,但它不会起作用,因为值类型将被装箱 - 即包装到引用类型的对象中.当您调用时Monitor.Exit(),它将再次装箱,因此它将尝试释放对其他对象引用的锁定.

关于你的代码示例:我认为它没有任何问题.您对stringList变量的所有访问权限都包含在a中lock,stringList除了在初始化期间,您永远不会分配给字段本身.这没有什么可以出错的; 它是线程安全的.(当然,如果某些其他代码在不锁定的情况下访问该字段,则可能会出错.如果您要将该字段设为公共字段,则很有可能会意外地发生这种情况.不需要仅使用本地范围的变量这样的锁,除非你真的不能确保你不能控制的代码不会访问该变量.)


Chr*_*ich 5

但幕后到底发生了什么?

请参阅这篇 MSDN 文章以获取深入的描述。

本质上,每个分配的 CLR 对象都有一个关联的字段,用于保存同步块索引。该索引指向 CLR 维护的同步块池。同步块保存与同步期间使用的关键部分相同的信息。最初,对象的同步块索引是没有意义的(未初始化)。然而,当您锁定该对象时,它会在池中获得有效的索引。

为什么只使用引用类型来锁定?

您需要一个引用类型,因为值类型没有关联的同步块索引字段(开销较小)。

即使用于完成锁定的对象发生了变化,为什么它仍然提供线程安全呢?

锁定 CLR 对象然后修改它类似于拥有一个带有CRITICAL_SECTION成员的 C++ 对象,该成员在修改同一对象时用于锁定。那里不存在线程安全问题。

在当前示例中,我们正在修改用于锁定目的的对象。理想情况下,这不是首选方法,最佳实践是使用专用的私有范围变量。

没错,文章中也描述了这种情况。如果您没有使用完全控制所属类的私有作用域变量,那么当两个单独的类决定使用lock同一引用对象时(例如,如果由于某种原因您stringList被传递给另一个类),您可能会遇到死锁问题然后也决定lock这样做)。这不太可能,但如果您使用严格控制的、永远不会传递的私有范围的变量,您将完全避免这种死锁。