2009-12-21 45 views
1

在服務器上的應用程序,我需要分配給每個連接的客戶端的唯一的ID,所以我做這種方式:2對鎖的性能問題/使用

private short GetFreeID() 
{ 
    lock (this.mUsedPlayerIDsSynchronization) 
    { 
     for (short I = 1; I < 500; I++) 
     { 
      if (ClientIDPool[I] == false) 
      { 
       ClientIDPool[I] = true; 
       return I; 
      } 
     } 
     return -1; 
    } 
} 

我的第一個問題:難道做更高效,我的意思是更好的表現?我在這裏讀到,我們應該學會編寫沒有鎖的代碼。我也讀過一些原子操作還有其他選項。 第二個問題:如果我想鎖定整個班級以便不允許在其中進行任何更改,該怎麼辦?例如:一個客戶端會更新第二個客戶端數據,我可以鎖定整個第二個客戶端類別,它是絕對被阻止的嗎?我仍然認爲「鎖定」只會確保其代碼段中的代碼當時只有一個線程進入,所以我不知道「鎖定(client2)」是否會導致該類中的任何內容都不能更改,直到此鎖定爲止釋放。

+0

如果您確實需要性能,任何形式的CPU輔助同步都會干擾它 - 完全停止。聯鎖指令比普通鎖更好 - 但僅限於短期。 (鎖定一個總線來改變一個4字節的值不是很好= D)。 – 2009-12-22 13:39:56

回答

11

鎖通常是最簡單的正確的方式,這是非常重要的。很多時候,如果有更高效的做事方式並不重要,只要你有明確的代碼並且它足夠好地執行。

這裏更高性能的方法,但是,這是要麼隨機生成一個GUID,或者如果你想重用的ID,有一個「池」(如LinkedList)未使用的ID。然後,您可以很快從池中取出,並在完成後將ID返回池(再次快速)。

或者,如果你真的只需要一個整數,並且它不是一個低的,你可以有一個靜態變量,從0開始,每次你只增加一個 - 你可以做到這一點沒有如果您希望使用Interlocked.Increment鎖定。我懷疑你會用完64位整數,例如:)

至於你的第二個問題:是的,鎖是諮詢。如果類中的所有內容在更改任何字段之前都會取出相同的鎖(並且這些字段是私有的),那麼可以防止其他代碼的行爲異常......但代碼確實需要取出該鎖的

編輯:如果你真的只需要一個整數,我仍然建議只使用Interlocked.Increment - 即使你的流量增加了1000倍,你也可以使用64位整數。但是,如果你想重用ID,那麼我建議創建一個新的類型來表示「池」。給一個已經創建了多少個計數器的計數器,這樣如果你用完了,你可以分配一個新的項目。然後,只需將可用的那些存儲在Queue<int>,LinkedList<int>Stack<int>(這不會很重要)。假設你可以相信你自己的代碼來合理回報的ID,就可以使API簡單:

int AllocateID() 
void ReturnID(int id) 

AllocateID會檢查是否池是空的,如果是這樣分配一個新的ID。否則,它會從池中刪除第一個條目並返回該條目。 ReturnID只會將指定的ID添加到池中。

+0

謝謝!那麼這些ID會發送給客戶端,因爲客戶端經常重新連接(通常每小時1000個),所以它必須是可重用的。我在你的「游泳池」解決方案中有點麻煩,你可以詳細說明一下嗎?謝謝! – Thomas 2009-12-21 13:23:20

+0

@Tomas:我不明白每小時1000個意味着他們必須是可重用的。即使你使用從0開始的Int32,那仍然會給你每小時1000個唯一ID的245 *年* ......使用Interlocked.Increment在這裏確實會更簡單。 – 2009-12-21 13:28:07

+0

是的,我不會用完ID,我很感興趣不向客戶發送大量數據,有很多操作。 – Thomas 2009-12-21 13:31:26

2

您在掃描數組時正在鎖定。

你最好有2堆。一個是免費ID,一個是ID正在使用。這樣,您可以彈出第一個堆棧中的一個,並將其推到第二個堆棧上。

這樣你鎖定的時間就少了很多。

1

我建議你使用GUID,除非你需要使用一個簡短的。 Guid.NewGuid()是線程安全的,因此您不需要鎖定或其他同步機制。

+0

如果最大值爲500,您仍然需要鎖定並檢查連接的客戶數 – Paolo 2009-12-21 13:23:07

+0

我需要儘可能少的數字,然後將這些數字發送回客戶端。 – Thomas 2009-12-21 13:27:24

+0

我的建議是基於您只需要一個唯一ID的假設。如果500限制和低數字是一個要求,你需要一個免費的ID池,像喬恩建議的那樣。 – 2009-12-21 13:34:57

1

你關心退回的身份證嗎?您可以使用Interlocked.Increment每次增加客戶端ID或生成GUID(前者可能會更快)。

然後使用一個簡單的計數器來跟蹤連接了多少個客戶端,而不是每次掃描數組。

2

You can allocate state on thread local memory.線程本地內存是線程安全的(只要你不通過指針)。

您可以使用兩個整數來生成唯一編號,並且只有一個是同步編號。

Integer 1:表示線程的遞增整數,每次初始化線程(應該是罕見的事件)時都會生成一個新的數字。

整數2:上線初始化開始在0

這個整數你會使用這兩個整數 - 這是存儲在線程局部存儲器 - 作爲唯一的整數,而整數2將增加通常(解鎖)。

通過這種方式,唯一整數的生成絕對是線程安全的 - 即,您不必使用原子CPU指令 - Interlocked.increment(這確實會導致硬件級性能損失)。

- 編輯:高速緩存一致性 -

from :

Cache一致性

爲了減少對內存訪問不同的緩存所需的時間 使用:最近訪問內存 在複製CPU緩存是 明顯快於普通的 內存。未來訪問相同的 地址將使用保存在高速緩存中的數據, 會減少獲取時間。的問題 出現在SMP(對稱 多處理)系統,那裏 幾個處理器具有自己的高速緩存 存儲器:當一個處理器改變在存儲器區域 變量,通過 同時若干處理器使用,它 實際上改變的一個自己的副本 變量,位於緩存中,而 共享變量仍具有原始值 的值。這個問題不能 通過使用一個 共享變量volatile關鍵字解決,因爲這樣只會 保證寫入到存儲器 指令將導致 程序存在,但仍然沒有相關規定 緩存操作。的 當然,也可以禁用CPU 緩存,映射內存作爲無緩存 (在 PAGE_NOCACHE保護標誌的VirtualAlloc()Win32 API函數), 但顯著放緩沿 這會帶來一定的侷限性:對於 例如,互鎖指令可能會在無緩存 內存上引發硬件異常。

對於存儲在緩存中的更多 的SMP系統數據的正確工作,所有緩存中的處理器應該都是相同的 。這意味着必須在硬件級**上同步CPU(保持 連貫性)的CPU 緩存**。但要注意的是高速緩存 同步(高速緩存相干性 交通流量)與程序執行由 異步它 是很重要的:** 當一個CPU改變的共享 變量的值的另一CPU暫時 觀察舊值。 這意味着CPU 將繼續執行,而不會等待 以執行高速緩存一致性操作,即完成 。此外,如果兩個 變量(a然後b)被 第一個CPU更改,另一個CPU可能會觀察到 b早於a變化。

聯鎖指令在這個 點有相當大的差異。確切地說,互鎖 指令是在鎖定總線下直接在物理 存儲器上製造 的命令。這意味着 該緩存不一致並不影響 地方共享 變量的程序,只有 互鎖指令(注意, 誰讀取 變量,並寫入了這兩個過程,即,應該 使用互鎖指令)進行訪問。

- 編輯:進一步clarrification: -

根據當前的設計互鎖遞增是indeed your best bet,卻是很不理想。你的CPU在片內有一個非常快的高速緩存(通常與CPU速度相同)。如果你的線程有本地內存,它將被拉進你的線程所在的CPU,這意味着你的CPU不必去主內存,它可以全速飛行。

如果使用互鎖遞增你的CPU將有

  1. 鎖定總線。
  2. 增加32位字。
  3. 釋放公共汽車。

你可以不這樣做。我可能看起來行事迂腐,因爲開銷可能只有be a 100% decrease in relative performance。然而,在一個具有4個物理CPU和16個內核的工業服務器應用程序中,這個UID發生器會在每次請求時觸發...相信我,您的總線將被擰緊。微優化是編程中的一個重要領域,尤其是當我們現在橫向擴展時。

+0

Interlocked.Increment是現代CPU上的單一CPU指令,與外部抓取鎖定然後遞增值不同。 – Paolo 2009-12-21 18:58:19

+0

是的,它是一條單指令,但它鎖定總線只是爲了更改一個單詞。 – 2009-12-22 13:23:47

+1

@Hassan:請記住這裏的背景 - 這不是*必須優化到一英寸內的東西。簡單性勝過微型優化,直到你遇到瓶頸,IMO。 – 2009-12-22 14:09:53