2012-09-03 48 views
1

我正在編寫一個小型多線程網絡服務器。所有經典的東西:它監聽傳入的連接,接受它們,然後在不同的線程中提供它們。此外,此服務器有時必須重新啓動,並且必須a)停止監聽,b)踢出所有連接的客戶端,c)調整某些設置/等待,d)恢復監聽。同步集合和中止任務

嗯,我幾乎不知道有關開發多線程程序的事情,所以我在尋求幫助。這裏是我來(核心的東西只):

class Server 
{ 
    class MyClient 
    { 
     Server server; 
     TcpClient client; 
     bool hasToFinish = false; 

     public MyClient(Server server, TcpClient client) 
     { 
      this.server = server; 
      this.client = client; 
     } 

     public void Go() 
     { 
      while (!hasToFinish) 
      { 
       // do all cool stuff 
      } 
      CleanUp(); 
     } 

     private void CleanUp() 
     { 
      // finish all stuff 

      client.Close(); 
      server.myClients.Remove(this); 
     } 

     public void Finish() 
     { 
      hasToFinish = true; 
     } 
    } 

    bool running = false; 
    TcpListener listener; 
    HashSet<MyClient> myClients = new HashSet<MyClient>(); 

    public void Start() 
    { 
     if (running) 
      return; 

     myClients.Clear(); 
     listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 1234); 
     listener.Start(); 
     listener.BeginAcceptTcpClient(AcceptClient, this); 
     running = true; 
    } 

    public void Stop() 
    { 
     if (!running) 
      return; 

     listener.Stop(); 
     foreach (MyClient client in myClients) 
     { 
      client.Finish(); 
     } 
     myClients.Clear(); 
     running = false; 
    } 

    public void AcceptClient(IAsyncResult ar) 
    { 
     MyClient client = new MyClient(this, ((TcpListener)ar.AsyncState).EndAcceptTcpClient(ar)); 
     myClients.Add(client); 
     client.Go(); 
    } 
} 

這絕對不滿意。沒有同步(我只是不知道該把它放在哪裏!),並且調用Server.Stop()不會使MyClient -s立即停止。我如何解決這些問題?

回答

1

該代碼看起來很乾淨,我們可以通過簡單的修改使其成爲線程安全的。

問題有三個部分,「客戶端」,「服務器」和客戶端 - 服務器交互。

客戶端首先,Go()方法由一個線程調用(我們稱之爲A),Finish()方法由另一個線程(B)調用。當線程B修改hasToFinish字段時,線程A可能不會立即看到修改,因爲該變量可能被緩存在CPU高速緩存中。我們可以通過將hasToFinish字段設置爲「volatile」來解決這個問題,這會強制線程B在更新時將變量更改發佈到線程A.

現在是服務器類。我建議你像下面的例子那樣在「服務器」實例上同步三種方法。它確保順序調用Start和Stop,並且它們所改變的變量將在線程間發佈。

客戶端 - 服務器交互也需要解決。在您的代碼中,客戶端從服務器中刪除其引用,但服務器以任何方式清除所有客戶端引用。它對我來說看起來多餘。如果我們可以刪除客戶端中的部分代碼,我們無需擔心。如果您選擇將邏輯保留在客戶端而不是出於某種原因在服務器中,請在Server類中創建公共方法調用RemoveClient(客戶端客戶端),並將其與Server實例同步。然後讓客戶端調用此方法而不是直接操作HashSet。

我希望這可以解決您的問題。

public void Start() 
{ 
    lock(this) 
    { 
    if (running) 
     return; 

    myClients.Clear(); 
    listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 1234); 
    listener.Start(); 
    listener.BeginAcceptTcpClient(AcceptClient, this); 
    running = true; 
    } 
} 

public void Stop() 
{ 
    lock(this) 
    { 
    if (!running) 
     return; 

    listener.Stop(); 
    foreach (MyClient client in myClients) 
    { 
     client.Finish(); 
    } 
    myClients.Clear(); 
    running = false; 
    } 
} 

public void AcceptClient(IAsyncResult ar) 
{ 
    lock(this) 
    { 
    MyClient client = new MyClient(this, ((TcpListener)ar.AsyncState).EndAcceptTcpClient(ar)); 
    myClients.Add(client); 
    client.Go(); 
    } 
} 
+0

順便說一句,如果你想弄清楚什麼時候使用volatile和何時使用鎖,我推薦書「實踐中的Java併發」。它解釋了爲什麼以及何時需要使用它們。這本書是關於Java的,但它也適用於C語言。 – nwang0