简体   繁体   English

优雅地结束正在等待阻塞队列的线程

[英]Gracefully ending a thread that's waiting on a blocking queue

I'm having an issue with a multi-threaded server I'm building as an academic exercise, more specifically with getting a connection to close down gracefully. 我遇到了一个多线程服务器的问题,我正在构建一个学术练习,更具体地说是获得一个优雅地关闭的连接。

Each connection is managed by a Session class. 每个连接都由Session类管理。 This class maintains 2 threads for the connection, a DownstreamThread and an UpstreamThread. 此类维护2个连接线程,一个DownstreamThread和一个UpstreamThread。

The UpstreamThread blocks on the client socket and encodes all incoming strings into messages to be passed up to another layer to deal with. UpstreamThread在客户端套接字上阻塞,并将所有传入的字符串编码为消息,以传递到另一层进行处理。 The DownstreamThread blocks on a BlockingQueue into which messages for the client are inserted. DownstreamThread阻塞BlockingQueue,其中插入了客户端的消息。 When there's a message on the queue, the Downstream thread takes the message off the queue, turns it into a string and sends it to the client. 当队列中有消息时,下游线程将消息从队列中取出,将其转换为字符串并将其发送到客户端。 In the final system, an application layer will act on incoming messages and push outgoing messages down to the server to send to the appropriate client, but for now I just have a simple application that sleeps for a second on an incoming message before echoing it back as an outgoing message with a timestamp appended. 在最终系统中,应用程序层将对传入的消息进行操作,并将传出的消息推送到服务器以发送到适当的客户端,但是现在我只有一个简单的应用程序,在传回消息之前会休息一秒钟作为附加时间戳的传出消息。

The problem I'm having is getting the whole thing to shut down gracefully when the client disconnects. 我遇到的问题是当客户端断开连接时,整个事情都会正常关闭。 The first issue I'm contending with is a normal disconnect, where the client lets the server know that it's ending the connection with a QUIT command. 我正在争论的第一个问题是正常的断开连接,客户端让服务器知道它正在结束与QUIT命令的连接。 The basic pseudocode is: 基本的伪代码是:

while (!quitting) {
    inputString = socket.readLine () // blocks
    if (inputString != "QUIT") {
        // forward the message upstream
        server.acceptMessage (inputString);
    } else {
        // Do cleanup
        quitting = true;
        socket.close ();
    }
}

The upstream thread's main loop looks at the input string. 上游线程的主循环查看输入字符串。 If it's QUIT the thread sets a flag to say that the client has ended communications and exits the loop. 如果是QUIT,则线程设置一个标志,表示客户端已结束通信并退出循环。 This leads to the upstream thread shutting down nicely. 这导致上游线程很好地关闭。

The downstream thread's main loop waits for messages in the BlockingQueue for as long as the connection closing flag isn't set. 只要未设置连接关闭标志,下游线程的主循环就会等待BlockingQueue中的消息。 When it is, the downstream thread is also supposed to terminate. 如果是,则下游线程也应该终止。 However, it doesn't, it just sits there waiting. 然而,它没有,它只是坐在那里等待。 Its psuedocode looks like this: 它的伪代码看起来像这样:

while (!quitting) {
    outputMessage = messageQueue.take (); // blocks
    sendMessageToClient (outputMessage);
}

When I tested this, I noticed that when the client quit, the upstream thread shut down, but the downstream thread didn't. 当我测试这个时,我注意到当客户端退出时,上游线程关闭,但下游线程没有。

After a bit of head scratching, I realised that the downstream thread is still blocking on the BlockingQueue waiting for an incoming message that will never come. 经过一番搔痒之后,我意识到下游线程仍在阻塞BlockingQueue,等待永远不会到来的传入消息。 The upstream thread doesn't forward the QUIT message any further up the chain. 上游线程不会在链上向前转发QUIT消息。

How can I make the downstream thread shut down gracefully? 如何正常关闭下游线程? The first idea that sprang to mind was setting a timeout on the take() call. 想到的第一个想法是在take()调用上设置超时。 I'm not too keen on this idea though, because whatever value you select, it's bound to be not entirely satisfactory. 我不是太热衷于这个想法,因为无论你选择什么价值,它一定不会完全令人满意。 Either it's too long and a zombie thread sits there for a long time before shutting down, or it's too short and connections that have idled for a few minutes but are still valid will be killed. 要么它太长了,僵尸线程在那里停留了很长时间才关闭,或者它太短了,连接已经闲置了几分钟但仍然有效将会被杀死。 I did think of sending the QUIT message up the chain, but that requires it to make a full round trip to the server, then the application, then back down to the server again and finally to the session. 我确实想过将QUIT消息发送到链中,但是这要求它完全往返服务器,然后是应用程序,然后再次返回服务器,最后返回到会话。 This doesn't seem like an elegant solution either. 这似乎也不是一个优雅的解决方案。

I did look at the documentation for Thread.stop() but that's apparently deprecated because it never worked properly anyway, so that looks like it's not really an option either. 我确实查看了Thread.stop()的文档但是显然已经弃用了,因为它无论如何都没有正常工作,所以看起来它也不是一个真正的选项。 Another idea I had was to force an exception to be triggered in the downstream thread somehow and let it clean up in its finally block, but this strikes me as a horrible and kludgey idea. 我的另一个想法是强制在下游线程中以某种方式触发异常并让它在其最终块中清理,但这让我觉得这是一个可怕而又笨拙的想法。

I feel that both threads should be able to gracefully shutdown on their own, but I also suspect that if one thread ends it must also signal the other thread to end in a more proactive way than simply setting a flag for the other thread to check. 我觉得两个线程都应该能够自己正常关闭,但我也怀疑如果一个线程结束,它还必须发出信号通知另一个线程以更加主动的方式结束,而不是简单地为另一个线程设置一个标志来检查。 As I'm still not very experienced with Java, I'm rather out of ideas at this point. 由于我对Java仍然不是很有经验,所以我现在很缺乏想法。 If anyone has any advice, it would be greatly appreciated. 如果有人有任何建议,将不胜感激。

For the sake of completeness, I've included the real code for the Session class below, though I believe the pseudocode snippets above cover the relevant parts of the problem. 为了完整起见,我在下面列出了Session类的真实代码,不过我相信上面的伪代码片段涵盖了问题的相关部分。 The full class is about 250 lines. 全班约250行。

import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.logging.*;

/**
 * Session class
 * 
 * A session manages the individual connection between a client and the server. 
 * It accepts input from the client and sends output to the client over the 
 * provided socket.
 * 
 */
public class Session {
    private Socket              clientSocket    = null;
    private Server              server          = null;
    private Integer             sessionId       = 0;
    private DownstreamThread    downstream      = null;
    private UpstreamThread      upstream        = null;
    private boolean             sessionEnding   = false;

    /**
     * This thread handles waiting for messages from the server and sending
     * them to the client
     */
    private class DownstreamThread implements Runnable {
        private BlockingQueue<DownstreamMessage>    incomingMessages    = null;
        private OutputStreamWriter                  streamWriter        = null;
        private Session                             outer               = null;

        @Override
        public void run () {
            DownstreamMessage message;
            Thread.currentThread ().setName ("DownstreamThread_" + outer.getId ());

            try {
                // Send connect message
                this.sendMessageToClient ("Hello, you are client " + outer.getId ());

                while (!outer.sessionEnding) {
                    message = this.incomingMessages.take ();
                    this.sendMessageToClient (message.getPayload ());
                }

                // Send disconnect message
                this.sendMessageToClient ("Goodbye, client " + getId ());

            } catch (InterruptedException | IOException ex) {
                Logger.getLogger (DownstreamThread.class.getName ()).log (Level.SEVERE, ex.getMessage (), ex);
            } finally {
                this.terminate ();
            }
        }

        /**
         * Add a message to the downstream queue
         * 
         * @param message
         * @return
         * @throws InterruptedException 
         */
        public DownstreamThread acceptMessage (DownstreamMessage message) throws InterruptedException {
            if (!outer.sessionEnding) {
                this.incomingMessages.put (message);
            }

            return this;
        }

        /**
         * Send the given message to the client
         * 
         * @param message
         * @throws IOException 
         */
        private DownstreamThread sendMessageToClient (CharSequence message) throws IOException {
            OutputStreamWriter osw;
            // Output to client
            if (null != (osw = this.getStreamWriter ())) {
                osw.write ((String) message);
                osw.write ("\r\n");
                osw.flush ();
            }

            return this;
        }

        /**
         * Perform session cleanup
         * 
         * @return 
         */
        private DownstreamThread terminate () {
            try {
                this.streamWriter.close ();
            } catch (IOException ex) {
                Logger.getLogger (DownstreamThread.class.getName ()).log (Level.SEVERE, ex.getMessage (), ex);
            }
            this.streamWriter   = null;

            return this;
        }

        /**
         * Get an output stream writer, initialize it if it's not active
         * 
         * @return A configured OutputStreamWriter object
         * @throws IOException 
         */
        private OutputStreamWriter getStreamWriter () throws IOException {
            if ((null == this.streamWriter) 
            && (!outer.sessionEnding)) {
                BufferedOutputStream os = new BufferedOutputStream (outer.clientSocket.getOutputStream ());
                this.streamWriter       = new OutputStreamWriter (os, "UTF8");
            }

            return this.streamWriter;
        }

        /**
         * 
         * @param outer 
         */
        public DownstreamThread (Session outer) {
            this.outer              = outer;
            this.incomingMessages   = new LinkedBlockingQueue ();
            System.out.println ("Class " + this.getClass () + " created");
        }
    }

    /**
     * This thread handles waiting for client input and sending it upstream
     */
    private class UpstreamThread implements Runnable {
        private Session outer   = null;

        @Override
        public void run () {
            StringBuffer    inputBuffer = new StringBuffer ();
            BufferedReader  inReader;

            Thread.currentThread ().setName ("UpstreamThread_" + outer.getId ());

            try {
                inReader    = new BufferedReader (new InputStreamReader (outer.clientSocket.getInputStream (), "UTF8"));

                while (!outer.sessionEnding) {
                    // Read whatever was in the input buffer
                    inputBuffer.delete (0, inputBuffer.length ());
                    inputBuffer.append (inReader.readLine ());
                    System.out.println ("Input message was: " + inputBuffer);

                    if (!inputBuffer.toString ().equals ("QUIT")) {
                        // Forward the message up the chain to the Server
                        outer.server.acceptMessage (new UpstreamMessage (sessionId, inputBuffer.toString ()));
                    } else {
                        // End the session
                        outer.sessionEnding = true;
                    }
                }

            } catch (IOException | InterruptedException e) {
                Logger.getLogger (Session.class.getName ()).log (Level.SEVERE, e.getMessage (), e);
            } finally {
                outer.terminate ();
                outer.server.deleteSession (outer.getId ());
            }
        }

        /**
         * Class constructor
         * 
         * The Core Java volume 1 book said that a constructor such as this 
         * should be implicitly created, but that doesn't seem to be the case!
         * 
         * @param outer 
         */
        public UpstreamThread (Session outer) {
            this.outer  = outer;
            System.out.println ("Class " + this.getClass () + " created");
        }
    }

    /**
     * Start the session threads
     */
    public void run () //throws InterruptedException 
    {
        Thread upThread     = new Thread (this.upstream);
        Thread downThread   = new Thread (this.downstream);

        upThread.start ();
        downThread.start ();
    }

    /**
     * Accept a message to send to the client
     * 
     * @param message
     * @return
     * @throws InterruptedException 
     */
    public Session acceptMessage (DownstreamMessage message) throws InterruptedException {
        this.downstream.acceptMessage (message);
        return this;
    }

    /**
     * Accept a message to send to the client
     * 
     * @param message
     * @return
     * @throws InterruptedException 
     */
    public Session acceptMessage (String message) throws InterruptedException {
        return this.acceptMessage (new DownstreamMessage (this.getId (), message));
    }

    /**
     * Terminate the client connection
     */
    private void terminate () {
        try {
            this.clientSocket.close ();
        } catch (IOException e) {
            Logger.getLogger (Session.class.getName ()).log (Level.SEVERE, e.getMessage (), e);
        }
    }

    /**
     * Get this Session's ID
     * 
     * @return The ID of this session
     */
    public Integer getId () {
        return this.sessionId;
    }

    /**
     * Session constructor
     * 
     * @param owner The Server object that owns this session
     * @param sessionId The unique ID this session will be given
     * @throws IOException 
     */
    public Session (Server owner, Socket clientSocket, Integer sessionId) throws IOException {

        this.server         = owner;
        this.clientSocket   = clientSocket;
        this.sessionId      = sessionId;
        this.upstream       = new UpstreamThread (this);
        this.downstream     = new DownstreamThread (this);

        System.out.println ("Class " + this.getClass () + " created");
        System.out.println ("Session ID is " + this.sessionId);
    }
}

Instead of calling Thread.stop use Thread.interrupt . 而不是调用Thread.stop使用Thread.interrupt That will cause the take method to throw an InterruptedException which you can use to know that you should shut down. 这将导致take方法抛出InterruptedException ,您可以使用它来知道您应该关闭。

Can you just create "fake" quit message instead of setting outer.sessionEnding to true when "QUIT" appears. 你可以创建“假”退出消息,而不是在出现“QUIT”时将outer.sessionEnding设置为true。 Putting this fake message in queue will wake the DownstreamThread and you can end it. 将此假消息放入队列将唤醒DownstreamThread,您可以结束它。 In that case you can even eliminate this sessionEnding variable. 在这种情况下,您甚至可以消除此sessionEnding变量。

In pseudo code this could look like this: 在伪代码中,这可能如下所示:

while (true) {
    outputMessage = messageQueue.take (); // blocks
    if (QUIT == outputMessage)
        break
    sendMessageToClient (outputMessage);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM