【问题标题】:Synchronizing collections and aborting tasks同步收集和中止任务
【发布时间】:2012-09-03 21:17:23
【问题描述】:

我正在编写一个小型多线程网络服务器。所有经典的东西:它监听传入的连接,接受它们,然后在不同的线程中服务它们。此外,此服务器有时必须重新启动,为此它必须 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 立即停止。我该如何解决这些问题?

【问题讨论】:

    标签: c# .net multithreading thread-safety synchronizing


    【解决方案1】:

    代码看起来很干净,我们可以通过简单的修改使其成为线程安全的。

    问题分为三个部分,“客户端”、“服务器”和客户端-服务器交互。

    首先是客户端,Go() 方法由一个线程(我们称之为 A)调用,而 Finish() 方法由另一个线程 (B) 调用。当线程 B 修改 hasToFinish 字段时,线程 A 可能不会立即看到修改,因为变量可能缓存在 CPU 缓存中。我们可以通过将 hasToFinish 字段设置为“volatile”来修复它,这会强制线程 B 在更新时将变量更改发布到线程 A。

    现在是服务器类。我建议您在“服务器”实例上同步三个方法,如下例所示。它确保 Start 和 Stop 被顺序调用,并且它们更改的变量跨线程发布。

    还需要解决客户端-服务器交互问题。在您的代码中,客户端从服务器中删除其引用,但服务器在 Finish() 以任何方式清除所有客户端引用。这对我来说看起来是多余的。如果我们可以删除客户端中的部分代码,我们就不用担心了。如果出于某种原因选择将逻辑保留在客户端而不是服务器中,请在 Server 类中创建一个名为 RemoveClient(Client client) 的公共方法,并将其与 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();
      }
    }
    

    【讨论】:

    • 顺便说一句,如果你想弄清楚何时使用 volatile 以及何时使用 lock,我推荐《Java 并发实践》一书。它解释了为什么以及何时需要使用它们。这本书是关于 Java 的,但它也适用于 C 语言。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-09-14
    • 1970-01-01
    • 1970-01-01
    • 2020-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多