簡體   English   中英

C# 手動鎖定/解鎖

[英]C# manual lock/unlock

我在 C# 中有一個 function 可以從多個線程中多次調用,我希望它只執行一次,所以我想到了這個:

class MyClass
{
    bool done = false;
    public void DoSomething()
    {
        lock(this)
            if(!done)
            {
                done = true;
                _DoSomething();
            }
    }
}

問題是_DoSomething需要很長時間,我不希望很多線程在看到done是真的時等待它。
這樣的事情可能是一種解決方法:

class MyClass
{
    bool done = false;
    public void DoSomething()
    {
        bool doIt = false;
        lock(this)
            if(!done)
                doIt = done = true;
        if(doIt)
             _DoSomething();
    }
}

但是手動進行鎖定和解鎖會好得多。
如何像lock(object)一樣手動鎖定和解鎖? 我需要它使用與lock相同的接口,以便這種手動方式和lock會相互阻塞(對於更復雜的情況)。

lock關鍵字只是 Monitor.Enter 和 Monitor.Exit 的語法糖:

Monitor.Enter(o);
try
{
    //put your code here
}
finally
{
    Monitor.Exit(o);
}

是相同的

lock(o)
{
    //put your code here
}

托馬斯建議在他的答案中仔細檢查鎖定。 這是有問題的。 首先,你不應該使用低鎖技術,除非你已經證明你有一個真正的性能問題可以通過低鎖技術解決 低鎖定技術非常難以正確使用。

其次,這是有問題的,因為我們不知道“_DoSomething”做了什么,也不知道我們將依賴它的行為的后果。

第三,正如我在上面的評論中指出的那樣,當另一個線程實際上仍在執行此操作時,返回 _DoSomething 已“完成”似乎很瘋狂。 我不明白你為什么有這個要求,我會假設這是一個錯誤。 即使我們在“_DoSomething”完成它之后設置“done”,這種模式的問題仍然存在。

考慮以下:

class MyClass 
{
     readonly object locker = new object();
     bool done = false;     
     public void DoSomething()     
     {         
        if (!done)
        {
            lock(locker)
            {
                if(!done)
                {
                    ReallyDoSomething();
                    done = true;
                }
            }
        }
    }

    int x;
    void ReallyDoSomething()
    {
        x = 123;
    }

    void DoIt()
    {
        DoSomething();
        int y = x;
        Debug.Assert(y == 123); // Can this fire?
    }

在 C# 的所有可能實現中,這個線程安全嗎? 我不認為它是。 請記住,處理器緩存可能會及時移動非易失性讀取 C# 語言保證易失性讀取相對於鎖等關鍵執行點的順序一致,並且它保證非易失性讀取在單個執行線程中是一致的,但它保證非易失性讀取在任何線程中都是一致的跨執行線程的方式。

讓我們看一個例子。

假設有兩個線程,Alpha 和 Bravo。 兩者都在 MyClass 的新實例上調用 DoIt。 怎么了?

在線程 Bravo 上,處理器緩存碰巧對 x 的 memory 位置進行(非易失性)提取。 其中包含零。 “完成”恰好位於 memory 的不同頁面上,該頁面尚未完全提取到緩存中。

在不同處理器 DoIt 上的“同時”線程 Alpha 上調用 DoSomething。 Thread Alpha 現在運行其中的所有內容。 當線程 Alpha 完成其工作時,done 為真,並且 x 在 Alpha 的處理器上為 123。 Thread Alpha 的處理器將這些事實刷新回主 memory。

Thread bravo 現在運行 DoSomething。 它將包含“完成”的主 memory 頁面讀取到處理器緩存中,並看到它是真的。

所以現在“完成”是真的,但“x”在線程 Bravo 的處理器緩存中仍然為零。 線程 Bravo 不需要使包含“x”為零的緩存部分無效,因為在線程 Bravo 上,“done”的讀取和“x”的讀取都不是易失性讀取。

雙重檢查鎖定的提議版本實際上根本不是雙重檢查鎖定。 當您更改雙重檢查鎖定模式時,您需要從頭開始重新開始並重新分析所有內容

使這個版本的模式正確的方法是至少將“完成”的第一次讀取變成易失性讀取。 那么“x”的讀取將不允許移動到易失性讀取的“前面”到“完成”。

你可以在鎖之前之后檢查done的值:

    if (!done)
    {
        lock(this)
        {
            if(!done)
            {
                done = true;
                _DoSomething();
            }
        }
    }

這樣,如果done為真,您將不會進入鎖。 鎖內的第二個檢查是為了應對競爭條件, if兩個線程同時進入第一個。

順便說一句,你不應該鎖定this ,因為它會導致死鎖。 改為鎖定私有字段(如private readonly object _syncLock = new object()

lock關鍵字只是Monitor class 的語法糖。 您也可以調用Monitor.Enter()Monitor.Exit()

但是 Monitor class 本身也具有TryEnter()Wait()可以幫助您解決問題的功能。

我知道這個答案來晚了幾年,但目前的答案似乎都沒有解決你的實際情況,只有在你發表評論后才變得明顯:

其他線程不需要使用由 RealDoSomething 生成的任何信息。

如果其他線程不需要等待操作完成,那么您問題中的第二個代碼片段就可以正常工作。 您可以通過完全消除鎖定並使用原子操作來進一步優化它:

private int done = 0;
public void DoSomething()
{
    if (Interlocked.Exchange(ref done, 1) == 0)   // only evaluates to true ONCE
        _DoSomething();
}

此外,如果您的_DoSomething()是一個即發即棄的操作,那么您甚至可能不需要第一個線程來等待它,從而允許它在線程池上的任務中異步運行:

int done = 0;

public void DoSomething()
{
    if (Interlocked.Exchange(ref done, 1) == 0)
        Task.Factory.StartNew(_DoSomething);
}

暫無
暫無

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

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