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': "simplecarnival",
    '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!\n")

    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\n")

    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\n")

        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()

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Browse other questions tagged or ask your own question.