Why does this multithreaded add-on not call the cleanup function when Blender exits?

I’m working on an add-on that has multithreading. I’ve created a stripped down version that has very little functionality in order to demonstrate the issue: http://simplecarnival.com/forum/simple_carnival_thread_test.py

I’m running this on Blender 2.78 on Windows. This add-on will write a test file at c:\junk.txt that demonstrates the various functions that the add-on reaches.

First, I’ll describe the problem. Install the add-on. Go to the 3D View. Open the T panel. Open the Misc tab. You’ll see a control there that says Thread Test, and a button that says “Enable”. Click Enable. The add-on will start running a thread. If you click the same button (which now says “Disable”), the add-on will stop running the thread. At this point, if you quit out of Blender, everything works as it should. No background thread is being run.

On the other hand, if you click the button when it says “Enable” (the text above the button will then say “*** ENABLED ***”) and then quit out of Blender, Blender will still be running in the background. You will need to go into Task Manager and manually kill the Blender process.

If you open the c:\junk.txt file that this add-on creates, you’ll see the various points of the add-on that have written to this file.

What should happen – because I’m importing atexit and setting up atexit.register(cleanup) – is that, when Blender quits, it should call that cleanup() function. At least, that’s how I understand atexit is supposed to work (though maybe it doesn’t work that way with Blender). If you look at the cleanup() function, it should theoretically write some text to the c:\junk.txt file. However, that text is never written, as cleanup() apparently is never called.

Any idea what is wrong with this code? Am I taking the wrong approach by using atexit and there’s another way I can run some code from an add-on before Blender exits?

Here’s the code:


bl_info = {
    'name': "Simple Carnival Thread Test",
    'author': "Jeff Boller",
    'version': (0, 0, 3),
    'blender': (2, 7, 8),
    'api': 44136,
    'location': "View3D > Left panel",
    'description': "Test for trying to get threads to clean up when shutting down Blender without disabling the add-on",
    'warning': "",
    'wiki_url': "",
    'tracker_url': "",
    'category': "Object"}

import sys
import time
import bpy
import threading
import socket
from bpy.props import *
from time import sleep
import re
import numpy
import math
import atexit


threadTest_running = False
thread_created = False
threadSocket = 0
socketServer = 0
receivedSocket = "none"
listening = False
socketMessages = []
shutDown = False
receivedData = ''

def cleanup():
    with open("c:\junk.txt", "a") as text_file:
        text_file.write("Cleanup was automatically called when shutting down...but this never gets called!
")

    global threadTest_running, threadSocket, listening, socketServer
    threadTest_running = False
    listening = False

    socketServer.settimeout(0.1)
    threadSocket.join()
    threadSocket.daemon = True
    socketServer.close()
    del socketServer

    thread_created = False
    shutDown = False

atexit.register(cleanup)

def create_thread():
    with open("c:\junk.txt", "a") as text_file:
        text_file.write("thread is about to be created
")

    global threadSocket,listening
    threadSocket = threading.Thread(name='threadSocket', target= socket_listen)
    listening = True
    create_socket_connection()
    threadSocket.start()

def socket_listen():
    global receivedSocket,listening, receivedData,socketServer, socketMessages
    socketServer.listen(5)
    while listening:
        (receivedSocket , adreess) = socketServer.accept()
        receivedData = (receivedSocket.recv(1024)).decode("utf-8")[:-2]
        socketMessages.append(receivedData)
        sleep(0.03)
        receivedSocket.close()

def create_socket_connection():
    global socketServer
    socketServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socketServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    socketServer.bind(('127.0.0.1',4000))

class open_threadtest(bpy.types.Operator):
    bl_idname = "threadtest_button.modal"
    bl_label = "Enable"
    _timer = None


    def modal(self, context, event):
        global threadTest_running, thread_created, listening, socketServer, socketMessages, shutDown
        result =  {'PASS_THROUGH'}

        if context.area.type == 'VIEW_3D' and threadTest_running and event.type == 'TIMER' :

            if shutDown:
                with open("c:\junk.txt", "a") as text_file:
                    text_file.write("We manually disabled the add-on
")

                threadTest_running = False
                listening = False

                socketServer.settimeout(0.01)
                #socketServer.shutdown(socket.SHUT_RDWR)
                socketServer.close()
                threadSocket.join()
                del socketServer
                context.window_manager.event_timer_remove(self._timer)

                thread_created = False
                shutDown = False
                result = {'CANCELLED'}
                self.report({'WARNING'}, "Thread Test has been disabled")

        return result

    def invoke(self, context, event):
        global threadTest_running,thread_created
        if context.area.type == 'VIEW_3D' and threadTest_running == False :
            self.cursor_on_handle = 'None'
            self._timer = context.window_manager.event_timer_add(0.01,context.window)
            threadTest_running = True
            context.window_manager.modal_handler_add(self)
            create_thread()
            thread_created = True
            return {'RUNNING_MODAL'}
        else:
            global shutDown
            shutDown = True
            return {'FINISHED'}

class AddBox(bpy.types.Operator):
    bl_idname = "threadtest.close"
    bl_label = "Disable"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global shutDown
        shutDown = True
        return {'FINISHED'}

class threadtest_panel(bpy.types.Panel):
    bl_label = "Thread Test"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    def draw(self, context):
        global receivedSocket,listening

        sce = context.scene
        layout = self.layout
        box = layout.box()

        if listening:
            box.label(text="*** ENABLED ***")
            box.operator("threadtest.close")
        else:
            box.label(text="Disabled")
            box.operator("threadtest_button.modal")


def register():
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

Technically atexit is waiting for python to exit, not blender, you may need another way to exit the thread.

Thanks for that bit of information. I guess then my followup question would be…is there an event that I can trap which is called as Blender is exiting?

Another idea. Since I have an already-running thread, is there a python command I could send to check to see if Blender is still running? If Blender is no longer running, then I can make the thread quit.

This had to be done because there could be people reading this in the future.
The handler is not being triggered because you first have to get rid of all the threads you’ve spawned.
That means that you have to create your threads with daemon=True

threadSocket = threading.Thread(name='threadSocket', target= socket_listen, daemon=True)

Mr. Kleiner, could you please write a longer post in which you fully explain the situation, what “daemon” actually is, and so on? More of a micro-tutorial? I don’t think that the original thread, five years ago, ever got properly settled or explained: they just stopped talking.

1 Like