[英]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.