[英]Lock-free Reference Counting
我正在開發一個需要廣泛 C API 互操作的系統。 互操作的一部分需要在任何操作之前和之后對相關系統進行初始化和關閉。 任何一個都不做都會導致系統不穩定。 我通過簡單地在核心一次性環境 class 中實現引用計數來完成此操作,如下所示:
public FooEnvironment()
{
lock(EnvironmentLock)
{
if(_initCount == 0)
{
Init(); // global startup
}
_initCount++;
}
}
private void Dispose(bool disposing)
{
if(_disposed)
return;
if(disposing)
{
lock(EnvironmentLock)
{
_initCount--;
if(_initCount == 0)
{
Term(); // global termination
}
}
}
}
這工作正常並實現了目標。 然而,由於任何互操作操作都必須嵌套在 FooEnvironment using 塊中,我們一直在鎖定,分析表明這種鎖定占運行時完成工作的近 50%。 在我看來,這是一個足夠基本的概念,.NET 或 CLR 中的某些內容必須解決它。 有沒有更好的方法來進行引用計數?
這是一項比您乍一看可能預想的更棘手的任務。 我認為 Interlocked.Increment 不足以完成您的任務。 相反,我希望您需要使用 CAS(比較和交換)執行一些魔法。
還要注意,很容易得到這個 mostly-right,但是當你的程序因 heisenbugs 而崩潰時 mostly-right 仍然是完全錯誤的。
在走這條路之前,我強烈建議進行一些真正的研究。 如果您搜索“Lock free reference counting”,幾個很好的起點會跳到頂部。 這篇 Dobbs 博士的文章很有用,而這個 SO 問題可能是相關的。
最重要的是,請記住無鎖編程很難。 如果這不是您的專長,請考慮退后一步並圍繞引用計數的粒度調整您的期望。 如果您不是專家,與創建可靠的無鎖機制相比,重新考慮您的基本重新計數策略可能要便宜得多。 特別是當您還不知道無鎖技術實際上會更快時。
正如哈羅德的評論所指出的那樣,答案是Interlocked
:
public FooEnvironment() {
if (Interlocked.Increment(ref _initCount) == 1) {
Init(); // global startup
}
}
private void Dispose(bool disposing) {
if(_disposed)
return;
if (disposing) {
if (0 == Interlocked.Decrement(ref _initCount)) {
Term(); // global termination
}
}
}
Increment
和Decrement
都返回新計數(僅針對這種用法),因此檢查不同。
但請注意:如果其他任何東西需要並發保護,這將不起作用。 Interlocked
操作本身是安全的,但沒有別的(包括Interlocked
調用的不同線程相對順序)。 在上面的代碼中, Init()
仍然可以在另一個線程完成構造函數后運行。
可能在 class 中使用一個通用的 static 變量。Static 只是一回事,並不特定於任何 object。
我相信這將為您提供一種使用 Interlocked.Increment/Decrement 的安全方式。
注意:這是過於簡單化了,如果 Init() 拋出異常,下面的代碼可能會導致死鎖。 當計數變為零時, Dispose
中也存在競爭條件,重置 init 並再次調用構造函數。 我不知道你的程序流程,所以如果你有可能在幾次 dispose 調用后再次啟動,那么你最好使用像SpinLock這樣更便宜的鎖而不是 InterlockedIncrement 。
static ManualResetEvent _inited = new ManualResetEvent(false);
public FooEnvironment()
{
if(Interlocked.Increment(ref _initCount) == 1)
{
Init(); // global startup
_inited.Set();
}
_inited.WaitOne();
}
private void Dispose(bool disposing)
{
if(_disposed)
return;
if(disposing)
{
if(Interlocked.Decrement(ref _initCount) == 0)
{
_inited.Reset();
Term(); // global termination
}
}
}
編輯:
在進一步考慮這個問題時,您可能需要考慮一些應用程序重新設計,而不是這個 class 來管理 Init 和 Term,只需在應用程序啟動時調用 Init 並在應用程序關閉時調用 Term,然后刪除需要完全鎖定,如果鎖定顯示為執行時間的 50%,那么您似乎總是想要調用 Init,所以只要調用它就可以了 go。
您可以使用以下代碼使其幾乎無鎖。 它肯定會降低爭用,如果這是您的主要問題,那將是您需要的解決方案。
此外,我建議從析構函數/終結器調用 Dispose(以防萬一)。 我已經更改了您的 Dispose 方法 - 無論disposing
參數如何,都應釋放非托管資源。 檢查此以了解有關如何正確處理 object 的詳細信息。
希望這對你有幫助。
public class FooEnvironment
{
private static int _initCount;
private static bool _initialized;
private static object _environmentLock = new object();
private bool _disposed;
public FooEnvironment()
{
Interlocked.Increment(ref _initCount);
if (_initCount > 0 && !_initialized)
{
lock (_environmentLock)
{
if (_initCount > 0 && !_initialized)
{
Init(); // global startup
_initialized = true;
}
}
}
}
private void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Dispose managed resources here
}
Interlocked.Decrement(ref _initCount);
if (_initCount <= 0 && _initialized)
{
lock (_environmentLock)
{
if (_initCount <= 0 && _initialized)
{
Term(); // global termination
_initialized = false;
}
}
}
_disposed = true;
}
~FooEnvironment()
{
Dispose(false);
}
}
使用Threading.Interlocked.Increment
會比獲取鎖、執行增量和釋放鎖快一點,但不會快很多。 多核系統上任一操作的昂貴部分是在內核之間強制同步 memory 緩存。 Interlocked.Increment
的主要優勢不是速度,而是它會在有限的時間內完成。 相比之下,如果一個人試圖獲取一個鎖,執行一個增量,然后釋放鎖,即使這個鎖除了保護計數器之外沒有其他用途,也存在一個風險,如果其他線程可能不得不永遠等待獲得鎖然后被伏擊。
您沒有提及您使用的是哪個版本的 .net,但是有一些Concurrent
類可能有用。 根據您分配和釋放事物的模式, ConcurrentBag
class 可能看起來有點棘手但可以正常工作的 class。它有點像隊列或堆棧,只是不能保證事物會以任何特定順序出現。 在您的資源包裝器中包含一個標志,指示它是否仍然有效,並在資源本身中包含對包裝器的引用。 創建資源用戶時,將包裝器 object 放入包中。 當不再需要資源用戶時,設置“無效”標志。 只要包中至少有一個包裝器 object 設置了“有效”標志,或者資源本身持有對有效包裝器的引用,資源就應該保持活動狀態。 如果在刪除項目時資源似乎沒有持有有效的包裝器,則獲取鎖,如果資源仍未持有有效的包裝器,則將包裝器從包中拉出直到找到有效的包裝器,然后將那個與資源一起存儲(或者,如果沒有找到,則銷毀資源)。 如果當一個項目被刪除時,資源擁有一個有效的包裝器,但包看起來可能包含過多的無效項目,獲取鎖,將包的內容復制到一個數組,並將有效的項目放回包中。 記下有多少物品被退回,這樣就可以判斷何時進行下一次清除。
這種方法可能看起來比使用鎖或Threading.Interlocked.Increment
更復雜,並且有很多極端情況需要擔心,但它可能會提供更好的性能,因為ConcurrentBag
旨在減少資源爭用。 如果處理器 1 在某個位置執行Interlocked.Increment
,然后處理器 2 這樣做,處理器 2 將必須指示處理器 1 從其緩存中刷新該位置,等到處理器 1 完成后,通知所有其他處理器它需要控制那個位置,把那個位置加載到它的緩存中,最后開始增加它。 在所有這些發生之后,如果處理器 1 需要再次遞增該位置,則將需要相同的一般步驟序列。 所有這一切都非常緩慢。 相比之下,ConcurrentBag class 的設計使多個處理器可以將內容添加到列表中而不會發生緩存沖突。 在添加內容和刪除內容之間的某個時間,必須將它們復制到連貫的數據結構中,但是可以以產生良好緩存性能的方式分批執行此類操作。
我沒有嘗試過使用ConcurrentBag
的上述方法,所以我不知道它實際會產生什么樣的性能,但根據使用模式,它可能會提供比通過引用計數獲得的性能更好的性能。
Interlocked class 方法比 lock 語句快一點,但在多核機器上速度優勢可能不是很大,因為 Interlocked 指令必須繞過 memory 緩存層。
在代碼未使用和/或程序退出時調用 Term() function 有多重要?
通常,您只需在包裝其他 API 的 class 的static 構造函數中調用一次 Init(),而不必擔心調用 Term()。 例如:
static FooEnvironment() {
Init(); // global startup
}
CLR 將確保 static 構造函數將在封閉的 class 中的任何其他成員函數之前被調用一次。
也可以掛鈎某些(但不是全部)應用程序關閉場景的通知,從而可以在干凈關閉時調用 Term()。 請參閱這篇文章。 http://www.codeproject.com/Articles/16164/Managed-Application-Shutdown
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.