From be19ebed15c1a3fe567e1c7384a5289dbba5ab1d Mon Sep 17 00:00:00 2001 From: Michael Hines Date: Fri, 22 Nov 2024 07:55:52 -0500 Subject: [PATCH 1/2] Fix thread sanitizer leak when from neuron import h, gui. --- share/lib/python/neuron/gui.py | 45 +++++++++++++++++----------------- src/nrnpython/inithoc.cpp | 9 +++++++ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/share/lib/python/neuron/gui.py b/share/lib/python/neuron/gui.py index 67d79ac787..2c69b8b8a2 100644 --- a/share/lib/python/neuron/gui.py +++ b/share/lib/python/neuron/gui.py @@ -7,14 +7,12 @@ Note that python threads are not used if nrniv is launched instead of Python """ - from neuron import h - from contextlib import contextmanager import threading import time +import atexit -# recursive, especially in case stop/start pairs called from doNotify code. _lock = threading.RLock() @@ -26,15 +24,6 @@ def start(): _lock.release() -""" -This allows one to temporarily disable the process_events loop using a construct -like: -from neuron import gui -with gui.disabled(): - something_that_would_interact_badly_with_process_events() -""" - - @contextmanager def disabled(): stop() @@ -48,9 +37,10 @@ def process_events(): _lock.acquire() try: h.doNotify() - except: - print("Exception in gui thread") - _lock.release() + except Exception as e: + print(f"Exception in gui thread: {e}") + finally: + _lock.release() class Timer: @@ -86,30 +76,41 @@ def end(self): class LoopTimer(threading.Thread): """ - a Timer that calls f every interval + A Timer that calls a function at regular intervals. """ def __init__(self, interval, fun): - """ - @param interval: time in seconds between call to fun() - @param fun: the function to call on timer update - """ self.started = False self.interval = interval self.fun = fun + self._running = threading.Event() threading.Thread.__init__(self, daemon=True) def run(self): h.nrniv_bind_thread(threading.current_thread().ident) self.started = True - while True: + self._running.set() + while self._running.is_set(): self.fun() time.sleep(self.interval) + def stop(self): + """Stop the timer thread and wait for it to terminate.""" + self._running.clear() + self.join() + -if h.nrnversion(9) == "2": # launched with python (instead of nrniv) +if h.nrnversion(9) == "2": # Launched with Python (instead of nrniv) timer = LoopTimer(0.1, process_events) timer.start() + + def cleanup(): + if timer.started: + print("Cleanup called: Stopping the GUI thread") + timer.stop() + + atexit.register(cleanup) + while not timer.started: time.sleep(0.001) diff --git a/src/nrnpython/inithoc.cpp b/src/nrnpython/inithoc.cpp index 9e9c9ef508..996168e12a 100644 --- a/src/nrnpython/inithoc.cpp +++ b/src/nrnpython/inithoc.cpp @@ -208,11 +208,20 @@ static int have_opt(const char* arg) { } void nrnpython_finalize() { + printf("nrnpython_finalize()\n"); #if NRN_ENABLE_THREADS if (main_thread_ == std::this_thread::get_id()) { #else { #endif + // Call python_gui_cleanup() if defined in Python + PyRun_SimpleString( + "try:\n" + " gui.cleanup()\n" + "except NameError:\n" + " pass\n"); + + // Finalize Python Py_Finalize(); } #if linux From 1bd8751209f98e8bbb8c7422feb916e6e4cbf10c Mon Sep 17 00:00:00 2001 From: Michael Hines Date: Tue, 26 Nov 2024 16:09:59 -0500 Subject: [PATCH 2/2] nrnpython_finalize must cleanup and Py_Finalize from MainThread --- .../lib/python/neuron/exec_in_main_thread.py | 65 +++++++++++++++++++ share/lib/python/neuron/gui.py | 35 ++++++++-- src/nrnpython/inithoc.cpp | 26 +++++--- 3 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 share/lib/python/neuron/exec_in_main_thread.py diff --git a/share/lib/python/neuron/exec_in_main_thread.py b/share/lib/python/neuron/exec_in_main_thread.py new file mode 100644 index 0000000000..7a3b67a64e --- /dev/null +++ b/share/lib/python/neuron/exec_in_main_thread.py @@ -0,0 +1,65 @@ +import threading +import queue +import sys +import signal +import readline + +# Queue for tasks to be executed in the main thread +task_queue = queue.Queue() + + +# Signal Handler to wake up the REPL to process tasks +def signal_handler(signum, frame): + _run_tasks() + + +def submit_task_to_main_thread(task, *args, **kwargs): + """Submit a task to be run in the main thread.""" + task_queue.put((task, args, kwargs)) + if sys.platform.startswith("win"): + _run_tasks() # Directly run if Windows + else: + # Send a signal to the main thread to ensure tasks are processed + signal.pthread_kill(threading.main_thread().ident, signal.SIGUSR1) + + +def _run_tasks(): + """Process tasks in the queue to be run in the main thread.""" + while not task_queue.empty(): + try: + task, args, kwargs = task_queue.get_nowait() + task(*args, **kwargs) + task_queue.task_done() + except queue.Empty: + break + + +# Pre-input hook function to process tasks before user input is taken +def pre_input_hook(): + _run_tasks() + readline.redisplay() + + +readline.set_pre_input_hook(pre_input_hook) + +# Catch signal for task processing +signal.signal(signal.SIGUSR1, signal_handler) + + +if __name__ == "__main__": + + def example_task(message): + print(f"Task executed: {message} in {threading.current_thread().name}") + + def submit(): + # Submit an example task from another thread + def submit_from_thread(): + print(f"enter submit_from_thread in {threading.current_thread().name}") + submit_task_to_main_thread(example_task, "Hello from ChildThread") + + # Example thread usage + child_thread = threading.Thread(target=submit_from_thread) + child_thread.start() + child_thread.join() + + submit() diff --git a/share/lib/python/neuron/gui.py b/share/lib/python/neuron/gui.py index 2c69b8b8a2..8689d195d9 100644 --- a/share/lib/python/neuron/gui.py +++ b/share/lib/python/neuron/gui.py @@ -13,6 +13,7 @@ import time import atexit +# recursive, especially in case stop/start pairs called from doNotify code. _lock = threading.RLock() @@ -24,6 +25,15 @@ def start(): _lock.release() +""" +This allows one to temporarily disable the process_events loop using a construct +like: +from neuron import gui +with gui.disabled(): + something_that_would_interact_badly_with_process_events() +""" + + @contextmanager def disabled(): stop() @@ -101,15 +111,32 @@ def stop(self): if h.nrnversion(9) == "2": # Launched with Python (instead of nrniv) + from neuron.exec_in_main_thread import submit_task_to_main_thread + timer = LoopTimer(0.1, process_events) timer.start() - def cleanup(): - if timer.started: - print("Cleanup called: Stopping the GUI thread") + def cleanup_(): + if timer.started and threading.current_thread() == threading.main_thread(): timer.stop() - atexit.register(cleanup) + def cleanup(): + submit_task_to_main_thread(cleanup_) + while timer._running.is_set(): + time.sleep(0.01) + + atexit.register(cleanup_) + + def finalize_(): + import ctypes + + if hasattr(ctypes.pythonapi, "Py_FinalizeEx"): + ctypes.pythonapi.Py_FinalizeEx() + + def finalize(): + submit_task_to_main_thread(finalize_) + while timer._running.is_set(): + time.sleep(0.01) while not timer.started: time.sleep(0.001) diff --git a/src/nrnpython/inithoc.cpp b/src/nrnpython/inithoc.cpp index 996168e12a..12b5b3d41c 100644 --- a/src/nrnpython/inithoc.cpp +++ b/src/nrnpython/inithoc.cpp @@ -208,25 +208,31 @@ static int have_opt(const char* arg) { } void nrnpython_finalize() { - printf("nrnpython_finalize()\n"); + // Try to call python_gui_cleanup() if defined in Python + PyRun_SimpleString( + "try:\n" + " gui.cleanup()\n" + "except NameError:\n" + " pass\n"); + #if NRN_ENABLE_THREADS if (main_thread_ == std::this_thread::get_id()) { #else - { + if (1) { #endif - // Call python_gui_cleanup() if defined in Python + Py_Finalize(); + } else { // in the gui thread PyRun_SimpleString( "try:\n" - " gui.cleanup()\n" + " gui.finalize()\n" "except NameError:\n" " pass\n"); - - // Finalize Python - Py_Finalize(); } -#if linux - if (system("stty sane > /dev/null 2>&1")) { - } // 'if' to avoid ignoring return value warning +#if __linux__ + int err = system("stty sane > /dev/null 2>&1"); + if (err) { + printf("stty sane returned %d\r\n", err); + } #endif }