3

Execution abruptly halting if the thread / process is killed makes sense

Why it won't execute cleanup code when I exit the main program normally by clicking the [X] on my terminal window?


I'm still learning the ins-and-outs of multithreaded applications, and I assume my problems come from not understanding how Python handles killing background threads.

Questions:

  1. Why won't my finally: block execute all the time?
  2. When else won't a finally: block execute?
  3. What happens to code execution inside a thread when the thread is killed?
  4. What happens to daemon/non-daemon threads when you exit the main process?

Details:

I'm trying to write a multithreaded program using ZMQ sockets that (among other things) writes stuff to a log file. I want the logging thread to unconditionally perform some messaging and clean-up right before it dies, but it won't most of the time.

The function below starts an infinite loop in a background thread and returns a zmq.PAIR socket for communication. The loop it starts listens to a socket, and anything written to that socket gets written to the file. The loop also (should) transmit back diagnostic messages like "I'm starting to log now!","Oops, there's been an error!" a "I'm exiting now". so the main program can keep tabs on it.

The main program generates a few threads using this pattern to monitor/control different bits and pieces. It polls several ZMQ sockets (connected to STDIN and a serial port) for messages, and forwards some of them to the socket connected to the file.

But now I'm stuck. The main program's routing & control logic works fine. get_logfile_sock's file writing works fine, and normal exception handling works as expected. But the "I'm exiting now" code doesn't execute when the thread is killed from the main program, or when I stop the main program altogether.

Example:

def get_logfile_sock(context, file_name):
    """
    Returns a ZMQ socket. Anything written to the socket gets appended to the a specified file. The socket will send diagnostic messages about file opening/closing and any exceptions encountered. 

    """

    def log_file_loop(socket):
        """
        Read characters from `socket` and write them to a file. Send back diagnostic and exception information.
        """
        try:
            socket.send("Starting Log File {}".format(file_name))
            with open(file_name, "a+") as fh:
                # File must start with a timestamp of when it was opened
                fh.write('[{}]'.format(get_timestamp()))
                # Write all strings/bytes to the file
                while True:
                    message = socket.recv()

                    fh.write(message)
                    fh.flush()

                    # Un-comment this line to demonstrate that the except: and finally: blocks both get executed when there's an error in the loop
                    # raise SystemExit

        except Exception as e:
            # This works fine when/if there's an exception in the loop
            socket.send("::".join(['FATALERROR', e.__class__.__name__, e.message]))
        finally:
            # This works fine if there's an exception raised in the loop
            # Why doesn't this get executed when my program exits? Isn't that just the main program raising SystemExit? 

            # Additional cleanup code goes here
            socket.send("Closing socket to log file {}".format(file_name))
            socket.close()


    # Make a socket pair for communication with the loop thread
    basename = os.path.basename(file_name).replace(":", "").replace(" ", "_").replace(".", "")
    SOCKNAME = 'inproc://logfile-{}'.format(basename)
    writer = context.socket(zmq.PAIR)
    reader = context.socket(zmq.PAIR)
    writer.bind(SOCKNAME)
    reader.connect(SOCKNAME)

    # Start the loop function in a separate thread
    thread = threading.Thread(target=log_file_loop, args=[writer])
    thread.daemon = True  # is this the right thing to do?
    thread.start()

    # Return a socket endpoint to the thread
    return reader
p.campbell
  • 98,673
  • 67
  • 256
  • 322
Matt Merrifield
  • 417
  • 6
  • 14

2 Answers2

2

doesn't execute when the thread is killed

Don't kill threads. Ask them nicely to exit and then join on them. Consider passing in a Condition for them to check.

Long answer: executing a kill will cause the thread to exit without guaranteeing that it complete any particular block and you should not expect good behavior of your system afterwards. It's probably a little safer to do this when using multiprocessing though.

Brian Cain
  • 14,403
  • 3
  • 50
  • 88
  • Besides using Condition, what are some different ways of asking a thread nicely to terminate itself? listening for a specific string (like "CMD::close") passed over ZMQ seems to work well. But more importantly, what happens from Python's perspective when you click the X on your terminal window, and the OS terminates the whole process? How can I capitalize on that to terminate the thread gracefully? – Matt Merrifield Oct 01 '14 at 19:55
  • @MattMerrifield Yes,creating a signalling-layer (which allows many things,incl.soft **SigKILL** signal) makes your inter-process messaging architecture "clean" & fully under your control.For **clicking on [X],right-top in the window-frame,there is a good solution available in Tkinter**,there one can assign this event with a specialised code ( anEventHANDLER ) that still can survive such killing-kiss and responsibly executes all dirty things ( incl. gracefully releasing all resource ) before the process dies upon being externally terminated by the OS. **Nb: Do not share / return ZMQ sockets** – user3666197 Oct 02 '14 at 17:41
  • @BrainCain. Asking threads to wait on a Condition doesn't seem compatible with having threads that are blocking waiting on network input, as they never check the Condition. – user48956 Sep 19 '17 at 01:20
1

How to enable try: / finally: work as needed

Best practice is to create an own signalling-layer ( which allows many things, incl. sending / receiving a soft SigKILL signal ).

That makes your inter-process messaging architecture "clean" & fully under your control.

Upon receiving a soft SigKILL, your thread code may handle all the necessary steps, incl. raising your own sub-type of exception(s), that make sense under your intended exception-related structure of:

try:
   # ... primary flow of a <code-block>-execution
   if ( SigINPUT == "SigKILL" ):
      raise SigKILL_EXCEPTION
except KeyboardInterrupt:
   # ... handle KeyboardInterrupt

except MemoryError:
   # ... handle MemoryError

except NotImplemented:
   # ... handle NotImplemented

except SigKILL_EXCEPTION:
   # ... handle SigKILL_EXCEPTION
   # situation-specific <code-block> shall rather be here, than in "finally:"
   
   # /\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
except:
   # ... handle *EXC
finally:
   # +++ ALWAYS DO THIS |||||||||||||||||||||||||||||||||||||||||||||||||||||
   #
   # ... a common <code-block> is ALWAYS executed, under all circumstances
   # ->  put an attempt to RETURN into SigKILL_EXCEPTION section a test this 
   # +++ ALWAYS DO THIS |||||||||||||||||||||||||||||||||||||||||||||||||||||

A demonstrator of finally: clause

def testTryFinally():
    try:
        print "TRY:"                   # show
        raise KeyboardInterrupt        # used to simulate SigKILL
    except    KeyboardInterrupt:       # EXC. to handle   SigKILL ( emulated by KBDI )
        print  "EXC.KBDI/SigKILL"                # show
        print  "EXC.KBDI:Going to RET(SigKILL)"  # remind the next instr. RET!!
        return "EXC.KBDI:RET(SigKILL)"           # execute RET <value1>
    except:                                      # EXC. collects all unhandled EXC-s
        print  "EXC.*"                           # show
    finally:                                     # FINALLY: clause
        print  "FINALLY: entered"                # show
    return     "RET(End)"                        # execute RET <value2>

>>> testTryFinally()
TRY:
EXC.KBDI/SigKILL
EXC.KBDI:Going to RET
FINALLY: entered
EXC.KBDI:RET(SigKILL)

How to execute a clean-up code once [x]-window-frame-icon got clicked

For handling a click on the [X]-window-frame-icon, right-top in the window-frame, there is a good solution available in Tkinter. There one can assign this event to be handled by a specialised code ( anEventHANDLER ) that still can survive such killing-kiss and which responsibly executes all dirty things ( incl. taking care to gracefully release all resources ) before the process dies upon being externally terminated by the OS.

Syntax:
win.protocol( 'WM_DELETE_WINDOW', lambda:None ) # blocks this way to terminate
win.protocol( 'WM_DELETE_WINDOW', aSendSigKILL_eventHANDLER )

Having created a soft-signalling between processes allows you to control and dispatch soft-SIGs so as to allow/enforce all distributed threads to get SIG-message and handle their own execution accordingly.

user3666197
  • 1
  • 6
  • 50
  • 92
  • Is there a portable way of doing the same thing? – user48956 Sep 19 '17 at 01:23
  • Well, after some 3 years, it is not much clear what do you ask about. Could you clarify the intent and a possible context of use? Saying "a portable way" is not of much explanatory power. Do not hesitate to be a bit more narrative, it helps to understand both your view and what are you asking about, ok? – user3666197 Sep 19 '17 at 03:14
  • Isn't your signaling here specific to MS windows. What if my app was for unix? – user48956 Sep 19 '17 at 13:46