2008-12-17 64 views
10

作爲一個副項目,我正在編寫一個我曾經玩過的古老遊戲的服務器。我試圖讓服務器儘可能鬆散耦合,但是我想知道什麼是多線程優秀的設計決策。目前,我有行動的順序如下:許多線程或儘可能少的線程?

  • 啓動(創建) - >
  • 服務器(偵聽客戶,創建) - >
  • 客戶端(監聽命令併發送週期數據)

我假設平均有100個客戶,因爲這是遊戲任何時間的最大值。對於整個線程的線程,什麼是正確的決定?我目前的設置如下:監聽新的連接,新的連接創建一個客戶端對象,並再次啓動監聽服務器上

  • 1個線程。
  • 客戶端對象有一個線程,偵聽傳入的命令併發送週期性數據。這是通過使用非阻塞套接字完成的,因此它只是檢查是否有數據可用,處理該數據,然後發送已排隊的消息。在發送 - 接收週期開始之前完成登錄。
  • 遊戲本身的一個線程(現在),因爲我認爲從整個客戶端 - 服務器部分分離,從架構上講。

這將導致總共102個線程。我甚至考慮給客戶端2個線程,一個用於發送,另一個用於接收。如果我這樣做,我可以在接收器線程上使用阻塞I/O,這意味着線程在平均情況下將大部分空閒。

我主要關心的是,通過使用這麼多線程,我會佔用資源。我不擔心競賽狀況或僵局,因爲這是我必須處理的事情。

我的設計的設置方式是我的可能使用一個單線程的所有客戶端通信,無論它是1還是100.我已經將通信邏輯從客戶端對象本身分開,所以我可以在不必重寫大量代碼的情況下實現它。

主要問題是:在應用程序中使用200多個線程是錯誤的嗎?它有優勢嗎?我正在考慮在多核機器上運行它,它是否需要像這樣的多核心的很多優勢?

謝謝!


在所有這些線程中,大多數線程通常都會被阻塞。我不希望連接超過每分鐘5次。來自客戶的命令很少出現,平均每分鐘20次。

按照我在這裏得到的答案來回答(上下文切換是我正在考慮的性能打擊,但直到您指出它時我才知道,謝謝!)我想我會去的方式有一個監聽器,一個接收器,一個發送器,以及其他一些東西;-)

回答

4

我寫在.NET,我不知道,如果這樣,我的代碼是由於.NET的限制和他們的API設計或者如果這是一個做事的標準方法,但是這是怎麼了,我做到了這一點過去的一些事情:

  • 將用於處理傳入數據的隊列對象。這應該在排隊線程和工作線程之間同步鎖定,以避免競爭狀況。

  • 用於處理隊列中數據的工作線程。排隊數據隊列的線程使用信號量來通知該線程處理隊列中的項目。該線程將在任何其他線程之前自行啓動,並且包含可以運行直到收到關閉請求的連續循環。循環中的第一條指令是暫停/繼續/終止處理的標誌。該標誌最初將被設置爲暫停,以便線程處於空閒狀態(而不是連續循環),而不需要執行任何處理。排隊線程將在隊列中有要處理的項目時更改該標誌。然後,該線程將在循環的每次迭代中處理隊列中的單個項目。當隊列爲空時,它會將標誌設置爲暫停狀態,以便在下一次循環迭代時它將等待,直到排隊過程通知它有更多工作要完成。

  • 一個連接監聽線程監聽傳入的連接請求並把這些關閉以...

  • 創建該連接/會話的連接處理線程。從連接監聽器線程中分離線程意味着您可以減少由於線程處理請求時資源減少而導致連接請求丟失的可能性。

  • 傳入數據監聽線程偵聽當前連接上接收的數據。所有數據都傳遞給排隊線程以排隊等待處理。您的監聽線程應儘可能少地進行基本監聽並將數據傳遞給處理。

  • 排隊線程按照正確的順序將數據排隊等候所有事情都可以正確處理,該線程將信號提升到處理隊列,讓它知道有數據需要處理。將此線程與傳入數據偵聽器分開意味着您不太可能錯過傳入數據。

  • 某些在方法之間傳遞的會話對象,以便每個用戶的會話都是獨立包含在整個線程模型中的。

這樣可以保持線程簡單,但是像我想象的那樣穩健。我很想找到一個比這更簡單的模型,但是我發現如果我嘗試並進一步減少線程模型,那麼我開始丟失網絡流上的數據或錯過連接請求。

它還協助TDD(測試驅動開發),使每個線程處理單個任務,並且更容易編碼測試。擁有數百個線程可能很快成爲資源分配的噩夢,同時擁有單個線程成爲維護的噩夢。

在每個邏輯任務中保持一個線程的方式要簡單得多,就像在TDD環境中每個任務有一個方法一樣,並且您可以在邏輯上區分每個應該做什麼。發現潛在問題更容易,修復它們也更容易。

7

使用的事件流/隊列和線程池來保持平衡;這將更好地適應其可能有更多或更少的內核

一般

其他機器,更多的活動的線程比你的核心將時間浪費在上下文切換

如果你的遊戲是由很多短的行動中,圓形/回收事件隊列會提供更好的性能比固定數量的線程

0

我想你應該問的問題不是200作爲一般的線程數是好還是壞,而是如何將這些線程的多是將變得活躍。

如果只有幾個人都活躍在任何給定的時刻,而所有其他正在睡覺或等待或諸如此類的東西,那麼你的罰款。在這種情況下,沉睡的線程不會讓你付出任何代價。

但是,如果所有的200個線程是活躍的,你將有你的CPU浪費了這麼多時間做線程上下文的所有〜200個線程之間切換。

+0

廢話,睡眠線程有一個1MB的堆棧,所以200個睡眠線程是200MB浪費的內存。 – 2008-12-17 18:21:33

+0

在一臺能夠管理遊戲中100個客戶端的服務器中,200MB的浪費空間幾乎就在那裏。 – 2008-12-17 18:37:19

2

什麼是您的平臺?如果是Windows,我會建議查看異步操作和線程池(或者如果您在C/C++中使用Win32 API級別,則直接使用I/O完成端口)。

這個想法是,你有少量的線程處理你的I/O,這使得你的系統能夠擴展到大量的併發連接,因爲連接數和線程數之間沒有關係由服務於他們的進程使用。如預期的那樣,.Net將你從細節中解脫出來,而Win32則不會。

使用異步I/O和這種服務器風格的挑戰是,客戶端請求的處理成爲服務器上的狀態機,數據到達觸發狀態更改。有時候,這需要一些習慣,但一旦你做了它真的很奇妙;)

我有一些免費的代碼,演示使用IOCP的C++的各種服務器設計here

如果您使用的是unix或需要跨平臺,並且使用C++,那麼您可能需要查看提供異步I/O功能的boost ASIO。

5

要簡單回答這個問題,在當今的硬件上使用200個線程是完全錯誤的。

每個線程都佔用1MB的內存,所以在開始做任何有用的事情之前,你需要佔用200MB的頁面文件。通過一切手段將您的操作分解爲可以在任何線程上安全運行的小塊,但將這些操作放在隊列中並且具有固定的有限數量的服務於這些隊列的工作線程。

更新:浪費200MB是否有用?在32位機器上,它是整個理論地址空間的10% - 沒有其他問題。在64位機器上,它聽起來像是在理論上可用的海洋中的一滴水,但實際上它仍然是一個非常大的塊(或者說,大量相當大的塊)的存儲被毫無意義地保留通過應用程序,然後必須由OS管理。它具有將每個客戶的有價值信息與大量無價值的填充信息包圍起來的作用,這些信息破壞了局部性,破壞了操作系統和CPU將頻繁訪問的東西保存在最快的緩存層中的企圖。

無論如何,內存浪費只是瘋狂的一部分。除非你有200個內核(和一個可以使用的操作系統),否則你實際上並沒有200個並行線程。你有(說)8個核心,每個核心瘋狂地切換25個線程。天真地你可能會認爲,由於這個原因,每個線程的體驗相當於運行速度慢25倍的核心。但實際上這比它糟糕得多 - 操作系統花費更多的時間將一個線程從核心轉移到另一個線程上(「上下文切換」),而不是實際上允許代碼運行。

只要看看任何知名的成功設計如何解決這類問題。 CLR的線程池(即使你不使用它)是一個很好的例子。它開始假設每個核心只有一個線程就足夠了。它允許創建更多,但只是爲了確保設計嚴格的並行算法最終能夠完成。它拒絕創建每秒超過2個線程,所以它通過減慢線程貪婪算法來有效懲罰線程。