[英]Do I need to synchronize resource access between Tasks with the default TaskScheduler?
[英]Do I need to synchronize thread access to an int?
我剛剛編寫了一個由多個線程同時調用的方法,我需要跟蹤所有線程何時完成。 代碼使用此模式:
private void RunReport()
{
_reportsRunning++;
try
{
//code to run the report
}
finally
{
_reportsRunning--;
}
}
這是代碼中唯一的_reportsRunning
值被更改的地方,該方法需要大約一秒鍾才能運行。
偶爾當我有超過六個左右的線程一起運行報告時,_reportsRunning的最終結果可以降到-1。 如果我在鎖中包含對_runningReports++
和_runningReports--
的調用,那么該行為似乎是正確且一致的。
所以,問題是:當我在C ++中學習多線程時,我被教導你不需要同步調用遞增和遞減操作,因為它們總是一個匯編指令,因此線程不可能在中間切換-呼叫。 我是否正確地教過,如果是這樣的話,那對C#來說怎么回事?
A ++
運算符在C#中不是原子的(我懷疑它在C ++中保證是原子的)所以是的,你的計數受競爭條件的影響。
使用Interlocked.Increment和.Decrement
System.Threading.Interlocked.Increment(ref _reportsRunning);
try
{
...
}
finally
{
System.Threading.Interlocked.Decrement(ref _reportsRunning);
}
所以,問題是:當我在C ++中學習多線程時,我被教導你不需要同步調用遞增和遞減操作,因為它們總是一個匯編指令,因此線程不可能在中間切換-呼叫。 我是否正確地教過,如果是這樣的話怎么會對C#不適用?
這是非常錯誤的。
在某些體系結構(如x86)上,有單個遞增和遞減指令。 許多架構沒有它們,需要單獨加載和存儲。 即使在x86上,也無法保證編譯器會生成這些指令的內存版本 - 它可能首先加載到寄存器中,特別是如果它需要對結果執行多個操作。
即使編譯器可以保證始終在x86上生成遞增和遞減的內存版本,但仍然不能保證原子性 - 兩個CPU可以同時修改變量並獲得不一致的結果。 該指令需要使用鎖定前綴來強制它成為原子操作 - 編譯器默認情況下從不發出鎖定變量,因為它的性能較差,因為它保證動作是原子的。
請考慮以下x86匯編指令:
inc [i]
如果我最初為0並且代碼在兩個核心上的兩個線程上運行,則兩個線程完成后的值可以合法地為1或2,因為無法保證一個線程將在另一個線程完成其寫入之前完成其讀取,或者在其他線程讀取之前,一個線程的寫入甚至可以看到。
將此更改為:
lock inc [i]
將導致最終值為2。
Win32的InterlockedIncrement
和InterlockedDecrement
以及.NET的Interlocked.Increment
和Interlocked.Decrement
導致執行lock inc
的等效(可能完全相同的機器代碼)。
你被教導錯了。
確實存在具有原子整數增量的硬件,因此您所教授的內容可能適用於您當時使用的硬件和編譯器。 但一般來說,在C ++中,你甚至不能保證遞增非易失性變量會在讀取內存時連續寫入內存,更不用說讀取原子。
增加int
是一條指令,但是如何在寄存器中加載值呢?
這就是i++
有效地做的事情:
正如您所看到的,有3個(在其他平台上可能會有所不同)指令,在任何階段,cpu都可以將上下文切換到不同的線程,使您的變量處於未知狀態。
您應該使用Interlocked.Increment和Interlocked.Decrement來解決這個問題。
不,您需要同步訪問權限。 在Windows上,您可以使用InterlockedIncrement()和InterlockedDecrement()輕松完成此操作。 我確信其他平台也有等價物。
編輯:剛剛注意到C#標簽。 做其他人說的話。 另請參閱: 我聽說i ++不是線程安全的,++ i是線程安全的嗎?
更高級語言中的任何類型的遞增/遞減操作(是的,甚至C
與機器指令相比更高級別)本質上不是原子的。 但是,每個處理器平台通常都具有支持各種原子操作的基元 。
如果您的講師指的是機器指令,則遞增和遞減操作可能是原子的。 然而,在當今不斷增加的多核平台上,這並不總是正確的,除非它們保證一致性 。
更高級別的語言通常使用低級原子機器指令實現對原子transactions
支持 。 這是由更高級API提供的互鎖機制。
x ++可能不是原子的,但++ x可能是(不確定的,但如果考慮后增量和預增量之間的差異,應該清楚為什么pre-更適合原子性)。
更重要的一點是,如果這些運行花費一秒鍾來運行每個運行,那么鎖定所添加的時間量將與方法本身的運行時間相比是噪聲。 在這種情況下嘗試移除鎖定可能不值得一試 - 你有一個正確的鎖定解決方案,這可能與非鎖定解決方案的性能沒有明顯差異。
machine, if one isn't using virtual memory, x++ (rvalue ignored) is likely to translate into a single atomic INC instruction on x86 architectures (if x is long, the operation is only atomic when using a 32-bit compiler). 在機器上,如果沒有使用虛擬內存,x ++(rvalue ignored)可能會轉換為x86體系結構上的單個原子INC指令(如果x很長,則使用32-時操作只是原子操作位編譯器)。 此外,movsb / movsw / movsl是移動字節/字/長字的原子方式; 編譯器不喜歡將它們用作分配變量的常規方法,但可以使用原子移動實用程序函數。 虛擬內存管理器有可能以這樣的方式編寫,即如果在寫入時發生頁面錯誤,那些指令將以原子方式運行,但我認為通常不會保證。
在多處理器機器上,除非使用顯式互鎖指令(通過特殊庫調用可調用),否則所有投注都將關閉。 通用的最通用的指令是CompareExchange。 該指令只有在包含預期值時才會改變內存位置; 當它決定是否改變它時,它將返回它所具有的值。 如果有人希望用1“變量”變量,可以做一些像(在vb.net中)
Dim OldValue as Integer Do OldValue = Variable While Threading.Interlocked.CompareExchange(Variable, OldValue Xor 1, OldValue) OldValue
這種方法允許對新變量應該依賴於舊值的變量執行任何種類的原子更新。 對於某些常見操作(如遞增和遞減),有更快的替代方法,但CompareExchange也允許實現其他有用的模式。
重要提示:(1)保持環路盡可能短; 循環時間越長,另一個任務在循環期間命中變量的可能性就越大,每次發生時浪費的時間就越多; (2)在線程之間任意划分的指定數量的更新將始終完成,因為線程可以強制重新執行循環的唯一方法是,如果某個其他線程已經取得了有用的進展; 但是,如果某些線程可以在不向前完成的情況下執行更新,則代碼可能會變為實時鎖定。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.