Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix thread sanitizer leak when launching Python 3.13 and using from neuron import h, gui. #3243

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions share/lib/python/neuron/exec_in_main_thread.py
Original file line number Diff line number Diff line change
@@ -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()

Check warning on line 13 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L13

Added line #L13 was not covered by tests


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

Check warning on line 20 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L18-L20

Added lines #L18 - L20 were not covered by tests
else:
# Send a signal to the main thread to ensure tasks are processed
signal.pthread_kill(threading.main_thread().ident, signal.SIGUSR1)

Check warning on line 23 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L23

Added line #L23 was not covered by tests


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

Check warning on line 34 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L28-L34

Added lines #L28 - L34 were not covered by tests


# Pre-input hook function to process tasks before user input is taken
def pre_input_hook():
_run_tasks()
readline.redisplay()

Check warning on line 40 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L39-L40

Added lines #L39 - L40 were not covered by tests


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}")

Check warning on line 52 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L51-L52

Added lines #L51 - L52 were not covered by tests

def submit():

Check warning on line 54 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L54

Added line #L54 was not covered by tests
# 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")

Check warning on line 58 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L56-L58

Added lines #L56 - L58 were not covered by tests

# Example thread usage
child_thread = threading.Thread(target=submit_from_thread)
child_thread.start()
child_thread.join()

Check warning on line 63 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L61-L63

Added lines #L61 - L63 were not covered by tests

submit()

Check warning on line 65 in share/lib/python/neuron/exec_in_main_thread.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/exec_in_main_thread.py#L65

Added line #L65 was not covered by tests
52 changes: 40 additions & 12 deletions share/lib/python/neuron/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
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()
Expand Down Expand Up @@ -48,9 +47,10 @@
_lock.acquire()
try:
h.doNotify()
except:
print("Exception in gui thread")
_lock.release()
except Exception as e:
print(f"Exception in gui thread: {e}")

Check warning on line 51 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L50-L51

Added lines #L50 - L51 were not covered by tests
finally:
_lock.release()


class Timer:
Expand Down Expand Up @@ -86,30 +86,58 @@

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

Check warning on line 110 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L109-L110

Added lines #L109 - L110 were not covered by tests


if h.nrnversion(9) == "2": # Launched with Python (instead of nrniv)
from neuron.exec_in_main_thread import submit_task_to_main_thread

if h.nrnversion(9) == "2": # launched with python (instead of nrniv)
timer = LoopTimer(0.1, process_events)
timer.start()

def cleanup_():
if timer.started and threading.current_thread() == threading.main_thread():
timer.stop()

Check warning on line 121 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L120-L121

Added lines #L120 - L121 were not covered by tests

def cleanup():
submit_task_to_main_thread(cleanup_)
while timer._running.is_set():
time.sleep(0.01)

Check warning on line 126 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L124-L126

Added lines #L124 - L126 were not covered by tests

atexit.register(cleanup_)

def finalize_():
import ctypes

Check warning on line 131 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L131

Added line #L131 was not covered by tests

if hasattr(ctypes.pythonapi, "Py_FinalizeEx"):
ctypes.pythonapi.Py_FinalizeEx()

Check warning on line 134 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L133-L134

Added lines #L133 - L134 were not covered by tests

def finalize():
submit_task_to_main_thread(finalize_)
while timer._running.is_set():
time.sleep(0.01)

Check warning on line 139 in share/lib/python/neuron/gui.py

View check run for this annotation

Codecov / codecov/patch

share/lib/python/neuron/gui.py#L137-L139

Added lines #L137 - L139 were not covered by tests

while not timer.started:
time.sleep(0.001)

Expand Down
23 changes: 19 additions & 4 deletions src/nrnpython/inithoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,31 @@
}

void nrnpython_finalize() {
// Try to call python_gui_cleanup() if defined in Python
PyRun_SimpleString(

Check warning on line 212 in src/nrnpython/inithoc.cpp

View check run for this annotation

Codecov / codecov/patch

src/nrnpython/inithoc.cpp#L212

Added line #L212 was not covered by tests
"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
Py_Finalize();
} else { // in the gui thread
PyRun_SimpleString(

Check warning on line 225 in src/nrnpython/inithoc.cpp

View check run for this annotation

Codecov / codecov/patch

src/nrnpython/inithoc.cpp#L225

Added line #L225 was not covered by tests
"try:\n"
" gui.finalize()\n"
"except NameError:\n"
" pass\n");
}
#if __linux__
int err = system("stty sane > /dev/null 2>&1");
if (err) {
printf("stty sane returned %d\r\n", err);

Check warning on line 234 in src/nrnpython/inithoc.cpp

View check run for this annotation

Codecov / codecov/patch

src/nrnpython/inithoc.cpp#L232-L234

Added lines #L232 - L234 were not covered by tests
}
#if linux
if (system("stty sane > /dev/null 2>&1")) {
} // 'if' to avoid ignoring return value warning
#endif
}

Expand Down
Loading