2009-05-12 31 views
8

Tôi gặp sự cố với Monitor.Wait và Monitor.Pulse lồng vào nhau trong máy chủ TCP đa luồng. Để chứng minh vấn đề của tôi, đây là mã máy chủ của tôi:Monitor.Wait/Pulse điều kiện chủng tộc trong một máy chủ đa luồng

public class Server 
{ 
    TcpListener listener; 
    Object sync; 
    IHandler handler; 
    bool running; 

    public Server(IHandler handler, int port) 
    { 
     this.handler = handler; 
     IPAddress address = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0]; 
     listener = new TcpListener(address, port); 
     sync = new Object(); 
     running = false; 
    } 

    public void Start() 
    { 
     Thread thread = new Thread(ThreadStart); 
     thread.Start(); 
    } 

    public void Stop() 
    { 
     lock (sync) 
     { 
      listener.Stop(); 
      running = false; 
      Monitor.Pulse(sync); 
     } 
    } 

    void ThreadStart() 
    { 
     if (!running) 
     { 
      listener.Start(); 
      running = true; 
      lock (sync) 
      { 
       while (running) 
       { 
        try 
        { 
         listener.BeginAcceptTcpClient(new AsyncCallback(Accept), listener); 
         Monitor.Wait(sync); // Release lock and wait for a pulse 
        } 
        catch (Exception e) 
        { 
         Console.WriteLine(e.Message); 
        } 
       } 
      } 
     } 
    } 

    void Accept(IAsyncResult result) 
    { 
     // Let the server continue listening 
     lock (sync) 
     { 
      Monitor.Pulse(sync); 
     } 

     if (running) 
     { 
      TcpListener listener = (TcpListener)result.AsyncState; 
      using (TcpClient client = listener.EndAcceptTcpClient(result)) 
      { 
       handler.Handle(client.GetStream()); 
      } 
     } 
    } 
} 

Và đây là mã khách hàng của tôi:

class Client 
{ 
    class EchoHandler : IHandler 
    { 
     public void Handle(Stream stream) 
     { 
      System.Console.Out.Write("Echo Handler: "); 
      StringBuilder sb = new StringBuilder(); 
      byte[] buffer = new byte[1024]; 
      int count = 0; 
      while ((count = stream.Read(buffer, 0, 1024)) > 0) 
      { 
       sb.Append(Encoding.ASCII.GetString(buffer, 0, count)); 
      } 
      System.Console.Out.WriteLine(sb.ToString()); 
      System.Console.Out.Flush(); 
     } 
    } 

    static IPAddress localhost = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0]; 

    public static int Main() 
    { 
     Server server1 = new Server(new EchoHandler(), 1000); 
     Server server2 = new Server(new EchoHandler(), 1001); 

     server1.Start(); 
     server2.Start(); 

     Console.WriteLine("Press return to test..."); 
     Console.ReadLine(); 

     // Note interleaved ports 
     SendMsg("Test1", 1000); 
     SendMsg("Test2", 1001); 
     SendMsg("Test3", 1000); 
     SendMsg("Test4", 1001); 
     SendMsg("Test5", 1000); 
     SendMsg("Test6", 1001); 
     SendMsg("Test7", 1000); 

     Console.WriteLine("Press return to terminate..."); 
     Console.ReadLine(); 

     server1.Stop(); 
     server2.Stop(); 

     return 0; 
    } 

    public static void SendMsg(String msg, int port) 
    { 
     IPEndPoint endPoint = new IPEndPoint(localhost, port); 

     byte[] buffer = Encoding.ASCII.GetBytes(msg); 
     using (Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) 
     { 
      s.Connect(endPoint); 
      s.Send(buffer); 
     } 
    } 
} 

Khách hàng gửi Bảy bài viết, nhưng máy chủ chỉ in bốn:

 
Press return to test... 

Press return to terminate... 
Echo Handler: Test1 
Echo Handler: Test3 
Echo Handler: Test2 
Echo Handler: Test4 

Tôi nghi ngờ màn hình đang bị nhầm lẫn bằng cách cho phép Pulse xảy ra (trong phương thức Accept của máy chủ) trước khi xảy ra Wait (i n phương pháp ThreadStart), mặc dù ThreadStart vẫn nên có khóa trên đối tượng sync cho đến khi nó gọi Monitor.Wait() và sau đó phương thức Accept có thể lấy khóa và gửi Pulse. Nếu bạn nhận xét ra hai dòng sau trong phương pháp Stop() của máy chủ:

//listener.Stop(); 
//running = false; 

Các thông điệp còn lại xuất hiện khi phương pháp Stop() của máy chủ được gọi (ví dụ: thức dậy đối tượng sync của máy chủ gây ra nó để gửi các tin nhắn gửi đến còn lại). Dường như với tôi điều này chỉ có thể xảy ra trong điều kiện đua giữa các phương pháp ThreadStartAccept, nhưng khóa xung quanh đối tượng sync sẽ ngăn điều này.

Bất kỳ ý tưởng nào?

Rất cám ơn, Simon.

ps. Lưu ý rằng tôi biết rằng đầu ra xuất hiện không đúng thứ tự vv, tôi đặc biệt hỏi về tình trạng cuộc đua giữa các khóa và Màn hình. Chúc mừng, SH.

Trả lời

5

Vấn đề là bạn đang sử dụng Pulse/Wait làm tín hiệu. Một tín hiệu thích hợp, chẳng hạn như AutoResetEvent có trạng thái sao cho nó vẫn được báo hiệu cho đến khi một luồng được gọi là WaitOne(). Gọi xung mà không có bất kỳ chủ đề chờ đợi trên nó sẽ trở thành một noop.

Điều này được kết hợp với thực tế là một khóa có thể được thực hiện nhiều lần bởi cùng một sợi. Vì bạn đang sử dụng chương trình Async, việc chấp nhận gọi lại có thể được gọi bởi cùng một luồng đã thực hiện BeginAcceptTcpClient.

Hãy để tôi minh họa. Tôi đã nhận xét máy chủ thứ hai và đã thay đổi một số mã trên máy chủ của bạn.

void ThreadStart() 
{ 
    if (!running) 
    { 
     listener.Start(); 
     running = true; 
     lock (sync) 
     { 
      while (running) 
      { 
       try 
       { 
        Console.WriteLine("BeginAccept [{0}]", 
         Thread.CurrentThread.ManagedThreadId); 
        listener.BeginAcceptTcpClient(new AsyncCallback(Accept), listener); 
        Console.WriteLine("Wait [{0}]", 
         Thread.CurrentThread.ManagedThreadId); 
        Monitor.Wait(sync); // Release lock and wait for a pulse 
       } 
       catch (Exception e) 
       { 
        Console.WriteLine(e.Message); 
       } 
      } 
     } 
    } 
} 

void Accept(IAsyncResult result) 
{ 
    // Let the server continue listening 
    lock (sync) 
    { 
     Console.WriteLine("Pulse [{0}]", 
      Thread.CurrentThread.ManagedThreadId); 
     Monitor.Pulse(sync); 
    } 
    if (running) 
    { 
     TcpListener localListener = (TcpListener)result.AsyncState; 
     using (TcpClient client = localListener.EndAcceptTcpClient(result)) 
     { 
      handler.Handle(client.GetStream()); 
     } 
    } 
} 

Kết quả từ lần chạy của tôi được hiển thị bên dưới. Nếu bạn tự chạy mã này, các giá trị sẽ khác nhau, nhưng nó sẽ giống nhau nói chung.

Press return to test... 
BeginAccept [3] 
Wait [3] 

Press return to terminate... 
Pulse [5] 
BeginAccept [3] 
Pulse [3] 
Echo Handler: Test1 
Echo Handler: Test3 
Wait [3] 

Như bạn có thể thấy có hai Pulse được gọi, một từ một luồng riêng biệt (Pulse [5]) đánh thức Wait đầu tiên. Thread 3 sau đó thực hiện một BeginAccept khác, nhưng có các kết nối đang chờ tới mà thread quyết định gọi ngay Acceptback callback. Vì Accept được gọi bởi cùng một luồng, Lock (sync) không chặn nhưng Pulse [3] ngay lập tức trên một hàng đợi thread rỗng.

Hai trình xử lý được gọi và xử lý hai thư.

Mọi thứ đều ổn, và ThreadStart bắt đầu chạy lại và đi đến Chờ vô thời hạn.

Bây giờ, vấn đề cơ bản ở đây là bạn đang cố gắng sử dụng màn hình làm tín hiệu. Vì nó không nhớ trạng thái Pulse thứ hai bị mất.

Nhưng có một giải pháp dễ dàng cho việc này. Sử dụng AutoResetEvents, đó là một tín hiệu thích hợp và nó sẽ ghi nhớ trạng thái của nó.

public Server(IHandler handler, int port) 
{ 
    this.handler = handler; 
    IPAddress address = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0]; 
    listener = new TcpListener(address, port); 
    running = false; 
    _event = new AutoResetEvent(false); 
} 

public void Start() 
{ 
    Thread thread = new Thread(ThreadStart); 
    thread.Start(); 
} 

public void Stop() 
{ 
    listener.Stop(); 
    running = false; 
    _event.Set(); 
} 

void ThreadStart() 
{ 
    if (!running) 
    { 
     listener.Start(); 
     running = true; 
     while (running) 
     { 
      try 
      { 
       listener.BeginAcceptTcpClient(new AsyncCallback(Accept), listener); 
       _event.WaitOne(); 
      } 
      catch (Exception e) 
      { 
       Console.WriteLine(e.Message); 
      } 
     } 
    } 
} 

void Accept(IAsyncResult result) 
{ 
    // Let the server continue listening 
    _event.Set(); 
    if (running) 
    { 
     TcpListener localListener = (TcpListener) result.AsyncState; 
     using (TcpClient client = localListener.EndAcceptTcpClient(result)) 
     { 
      handler.Handle(client.GetStream()); 
     } 
    } 
} 
+0

Cảm ơn Mats. Tôi cho rằng BeginAcceptTcpClient luôn chạy trên một chuỗi riêng biệt và do đó tôi có thể sử dụng đối tượng đồng bộ hóa như một phần quan trọng. Bạn đã được tại chỗ trên và tín hiệu là con đường để đi. Cảm ơn một lần nữa. SH –

Các vấn đề liên quan