簡體   English   中英

為什么鎖需要 C# 中的實例?

[英]Why Do Locks Require Instances In C#?

為每個鎖 object 使用 object 實例的目的是什么? CLR 是否存儲了一個 object 的實例,該實例由一個線程在調用Monitor.Enter(instance)時傳遞,以便當另一個線程嘗試進入鎖時,CLR 將檢查新線程提供的實例以及該實例是否匹配到第一個線程實例,然后 CLR 會將新線程添加到第一個服務隊列中的第一個,等等?

CLR 是否存儲了一個 object 的實例,該實例由一個線程在調用 Monitor.Enter(instance) 時傳遞,以便當另一個線程嘗試進入鎖時,CLR 將檢查新線程提供的實例以及該實例是否匹配到第一個線程實例,然后 CLR 會將新線程添加到第一個服務隊列中的第一個,等等?

忽略由抖動執行的指令重新排序和其他魔術。

首先,讓我們解決問題的重要部分:

為什么鎖需要 C# 中的實例?

答案並不那么令人滿意,但歸結為……嗯,它必須以某種方式完成!

您可以想象C# 規范CLR可以使用魔術字符串數字來跟蹤線程同步,但設計人員選擇使用引用類型 引用類型已經有一個header用於其他 CLR 活動,因此他們沒有給你保留在表中的幻數字符串,而是選擇了雙重用途header 作為引用類型來跟蹤線程同步 故事基本結束。


更長的故事

Monitor鎖對象需要是引用類型 值類型沒有像Reference Types這樣的標頭,部分原因是它們不需要終結並且不能被 GC 固定。 此外,值類型可以被裝箱,這基本上意味着它們被包裝到object中。 當您將值類型傳遞給Monitor時,它們會被裝箱,當您傳遞相同的值類型時,它們會被裝箱到不同的 object 中(這否定了lock的所有內部 CLR 管道)。

這主要是為什么值類型不能用於鎖定的原因......

讓我們繼續前進

值類型引用類型都具有內部 memory 布局。 但是,引用類型還包含一個 32 位header以幫助 CLR 在object上執行某些內務處理任務(如上所述)。 這就是我們將要談論的

header 中發生了相當多的事情,但它與火箭科學相去甚遠。 雖然,關於鎖定,這里只有兩個概念很重要,header Lock State信息或 header 是否需要膨脹到同步表

Object header

典型 object header 格式中的最高有效字節如下所示。

|31            0|    
----------------| 
|7|6|5|4|3|2| --|
 | | | | | |
 | | | | | +- BIT_SBLK_IS_HASHCODE : set if the rest of the word is a hash code (or sync block index)
 | | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX : set if hashcode or sync block index is set
 | | | +----- BIT_SBLK_SPIN_LOCK : lock the header for exclusive mutation on spin
 | | +------- BIT_SBLK_GC_RESERVE : set if the object is pinned
 | +--------- BIT_SBLK_FINALIZER_RUN : set if finalized already
 +----------- BIT_SBLK_AGILE_IN_PROGRESS : set if locking on AppDomain agile classes

header 負責為 CLR 保存某些易於訪問的信息,主要是用於GC的微小數據位,是否已生成 HashCode 以及 ZA8CFDE6331BD49EB2AC96F8911 的State 但是,由於object header (32 位)中只有有限的大小,因此 header 可能需要膨脹同步塊表 這通常會在以下情況下完成。

  • 已生成 Hash 代碼並已獲取瘦鎖。
  • 已獲得 Fat Lock
  • 涉及條件變量(通過等待、脈沖等)

header 還不夠大。

鎖 State

object上創建后, CLR將查看 header 並首先確定是否需要在同步塊表中找到任何鎖定信息,它只需查看設置的位即可。 如果沒有Thin Lock ,它將創建一個(如果適用)。 如果有薄鎖,它將嘗試旋轉並等待它。 如果 header 已膨脹,它將在同步塊表中查找鎖定信息(待續......)。

Locking 有 2 種不同的風格。 關鍵區域條件變量

  • 關鍵區域EnterExitLock等的結果
  • 條件變量WaitPulse等的結果,這是另一個故事,因為它與問題無關。

關於關鍵區域,CLR 可以通過兩種主要方式鎖定它們。 薄鎖肥鎖 CLR 在混合鎖 model 中使用這兩者,這基本上意味着它先嘗試一個然后回退到下一個。

薄鎖

Object 薄鎖 Header

|31       |26                |15                   |9         0|
----------------------------------------------------------------
|7|6|5|4|3| App Domain Index | Lock Recusion Level | Thread id |
 | | | | | 
 | | | | | 
 | | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX = 0 can store a thin lock

Thin Lock基本上由App Domain IndexRecursion LevelManaged Thread Id組成。 線程 ID由鎖定線程原子設置,如果為零,或者如果非零,則使用簡單的自旋等待多次重新讀取鎖 state 以獲取鎖。 如果在一段時間后鎖仍然不可用,則需要提升鎖(如果尚未這樣做),將Thin Lock膨脹到同步塊表,並且需要向同步塊表注冊一個真正的 * 鎖基於 Kernel 事件(如自動復位事件)進行操作。

Thin Lock正是它聽起來的樣子,它是一種重量更輕、速度更快的機制,但它的代價是旋轉核心來完成它的工作。 這種混合鎖定機制在短期發布場景中速度更快,效率更低,但是 CLR 回退到資源密集度較低的較慢 kernel 鎖,用於更長的爭用場景。 簡而言之,總體而言,它通常會在日常使用中獲得更好的結果。

脂肪鎖

如果發生爭用或涉及條件變量(通過等待、脈沖等),則需要將其他信息存儲在同步塊中,例如 kernel object 的句柄或與鎖。 胖鎖正是它聽起來的樣子,它是一種更具侵略性的鎖,它速度較慢但資源密集度較低,因為它不會圍繞 CPU 不必要地旋轉,它更適合更長的鎖周期。

同步塊表

Object 同步塊索引 header

|31         |25               0|
--------------------------------
|7|6|5|4|3|2| Sync Block Index |
 | | | | | |
 | | | | | +- BIT_SBLK_IS_HASHCODE = 0 sync block index
 | | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX = 1 hash code or sync block index

CLR 在堆上有一個預先初始化的、可回收的、緩存的和可重用的同步塊表 此表可能包含 Hash 代碼(從標頭遷移),以及對象 Header同步塊索引(當提升/膨脹發生時)引用的各種類型的鎖定信息。

把它們放在一起*

Monitor.Enter被調用時,CLR 通過將當前線程 ID(除其他外)存儲在 object header(如討論)或將其提升到Sycnc Block Table來注冊獲取。 如果存在Thin Lock ,CLR 將通過檢查 header 或Sync Block Table短暫地使用自旋來等待鎖被解除競爭。

如果自旋鎖在自旋一定次數后仍無法獲得鎖,則可能最終需要向操作系統注冊一個自動重置事件,並將句柄存儲在Sync Block Table中。 此時,等待線程將只等待該句柄。


那么CLR會將新線程添加到先服務隊列中,等等?

不,沒有這樣的隊列,隨后這一切都可能導致不公平的行為。 線程有能力竊取信號和喚醒之間的鎖,但是 CLR 確實以有序的方式幫助了這一點,並嘗試阻止 [鎖護衛隊][3]。

因此,這里顯然還有很多關於鎖的類型(關鍵區域和條件變量)、CLR memory model、回調如何工作等等。 但它應該給你一個起點來回答你最初的問題

免責聲明:許多此類信息實際上可能會發生變化,因為它們是 CLR 實現細節。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM