Skip to content

Commit 2868e1c

Browse files
authored
Merge pull request InsightSoftwareConsortium#6022 from thewtex/release-gil-5.4
ENH: Add ITK_PYTHON_RELEASE_GIL option and SWIG -threads flag
2 parents 4ac84ca + f876cbd commit 2868e1c

4 files changed

Lines changed: 153 additions & 8 deletions

File tree

Wrapping/Generators/Python/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,17 @@ macro(
275275
"${doc_file}")
276276
endif()
277277

278+
# Conditionally add -threads flag to release the GIL during ITK operations
279+
set(_swig_threads_flag "")
280+
if(ITK_PYTHON_RELEASE_GIL)
281+
set(_swig_threads_flag "-threads")
282+
endif()
283+
278284
add_custom_command(
279285
OUTPUT ${cpp_file} ${python_file}
280286
COMMAND
281-
${swig_command} -c++ -python -fastdispatch -fvirtual -features autodoc=2 -doxygen -Werror
287+
${swig_command} -c++ -python ${_swig_threads_flag} -fastdispatch -fvirtual
288+
-features autodoc=2 -doxygen -Werror
282289
-w302 # Identifier 'name' redefined (ignored)
283290
-w303 # %extend defined for an undeclared class 'name' (to avoid warning about customization in pyBase.i)
284291
-w312 # Unnamed nested class not currently supported (ignored)
@@ -306,6 +313,7 @@ macro(
306313

307314
unset(dependencies)
308315
unset(swig_command)
316+
unset(_swig_threads_flag)
309317
endmacro()
310318

311319
macro(itk_end_wrap_submodule_python group_name)

Wrapping/Generators/Python/Tests/CMakeLists.txt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,20 @@ itk_python_add_test(
249249
${ITK_TEST_OUTPUT_DIR}/TestVLV.seg.nrrd
250250
DATA{${WrapITK_SOURCE_DIR}/images/TestVLV.seg.nrrd}
251251
COMMAND
252-
${CMAKE_CURRENT_SOURCE_DIR}/readWriteVLV.py
253-
DATA{${WrapITK_SOURCE_DIR}/images/TestVLV.seg.nrrd}
254-
${ITK_TEST_OUTPUT_DIR}/TestVLV.seg.nrrd
255-
59
256-
85
257-
58
258-
5)
252+
${CMAKE_CURRENT_SOURCE_DIR}/readWriteVLV.py
253+
DATA{${WrapITK_SOURCE_DIR}/images/TestVLV.seg.nrrd}
254+
${ITK_TEST_OUTPUT_DIR}/TestVLV.seg.nrrd
255+
59
256+
85
257+
58
258+
5
259+
)
260+
261+
if(ITK_PYTHON_RELEASE_GIL)
262+
# Test GIL release during ITK operations
263+
itk_python_add_test(
264+
NAME PythonGILReleaseTest
265+
COMMAND
266+
${CMAKE_CURRENT_SOURCE_DIR}/test_gil_release.py
267+
)
268+
endif()
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Test that the Python Global Interpreter Lock (GIL) is released during ITK operations.
3+
4+
This test verifies that when ITK_PYTHON_RELEASE_GIL is enabled, multiple Python threads
5+
can execute ITK operations concurrently.
6+
"""
7+
8+
import sys
9+
import threading
10+
import time
11+
12+
# Threshold for determining if parallel execution is significantly faster than sequential
13+
# With 4 outer threads and 1 ITK thread per filter, we expect at least 2x speedup
14+
# A value of 0.5 means parallel execution should be at most 50% of sequential time
15+
# This accounts for threading overhead and ensures GIL is being released
16+
PARALLEL_SPEEDUP_THRESHOLD = 0.5
17+
18+
19+
def test_gil_release():
20+
"""Test that GIL is released during ITK operations."""
21+
try:
22+
import itk
23+
except ImportError:
24+
print("ITK not available, skipping GIL release test")
25+
sys.exit(0)
26+
27+
# Create a simple test image
28+
image_type = itk.Image[itk.F, 2]
29+
size = [100, 100]
30+
31+
# Shared counter to track concurrent execution
32+
execution_times = []
33+
lock = threading.Lock()
34+
35+
def run_filter():
36+
"""Run an ITK filter operation that should release the GIL."""
37+
# Create an image
38+
image = itk.Image[itk.F, 2].New()
39+
region = itk.ImageRegion[2]()
40+
region.SetSize(size)
41+
image.SetRegions(region)
42+
image.Allocate()
43+
image.FillBuffer(1.0)
44+
45+
start_time = time.time()
46+
47+
# Run a computationally intensive filter
48+
# MedianImageFilter is a good test as it performs actual computation
49+
median_filter = itk.MedianImageFilter[image_type, image_type].New()
50+
median_filter.SetInput(image)
51+
median_filter.SetRadius(5)
52+
# Limit ITK internal threads to 1 to make the test more reliable
53+
median_filter.SetNumberOfWorkUnits(1)
54+
median_filter.Update()
55+
56+
end_time = time.time()
57+
58+
with lock:
59+
execution_times.append((start_time, end_time))
60+
61+
# Run multiple threads
62+
num_threads = 4
63+
threads = []
64+
65+
overall_start = time.time()
66+
67+
for _ in range(num_threads):
68+
thread = threading.Thread(target=run_filter)
69+
thread.start()
70+
threads.append(thread)
71+
72+
for thread in threads:
73+
thread.join()
74+
75+
overall_end = time.time()
76+
77+
# If GIL is properly released, the threads should have overlapping execution times
78+
# and the total time should be less than the sum of individual execution times
79+
80+
total_sequential_time = sum(end - start for start, end in execution_times)
81+
total_parallel_time = overall_end - overall_start
82+
83+
print(f"Total sequential time if run serially: {total_sequential_time:.3f}s")
84+
print(f"Total parallel time: {total_parallel_time:.3f}s")
85+
86+
# Check for overlap in execution times
87+
has_overlap = False
88+
if len(execution_times) >= 2:
89+
for i in range(len(execution_times)):
90+
for j in range(i + 1, len(execution_times)):
91+
start1, end1 = execution_times[i]
92+
start2, end2 = execution_times[j]
93+
# Check if there's any overlap
94+
if (start1 <= start2 < end1) or (start2 <= start1 < end2):
95+
has_overlap = True
96+
break
97+
if has_overlap:
98+
break
99+
100+
if has_overlap:
101+
print("SUCCESS: Thread execution times overlap - GIL appears to be released")
102+
return 0
103+
else:
104+
# Even without overlap, if parallel time is significantly less than sequential,
105+
# it suggests concurrent execution
106+
if total_parallel_time < total_sequential_time * PARALLEL_SPEEDUP_THRESHOLD:
107+
print("SUCCESS: Parallel execution is faster - GIL appears to be released")
108+
return 0
109+
else:
110+
print("FAILURE: No clear evidence of concurrent execution")
111+
print("This indicates that GIL is not being released properly")
112+
print(
113+
f"Expected parallel time < {total_sequential_time * PARALLEL_SPEEDUP_THRESHOLD:.3f}s, got {total_parallel_time:.3f}s"
114+
)
115+
return 1
116+
117+
118+
if __name__ == "__main__":
119+
sys.exit(test_gil_release())

Wrapping/WrappingOptions.cmake

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ else()
1717
CACHE INTERNAL "Build external languages support" FORCE)
1818
endif()
1919

20+
cmake_dependent_option(
21+
ITK_PYTHON_RELEASE_GIL
22+
"Release Python Global Interpreter Lock (GIL) during ITK operations"
23+
ON
24+
"ITK_WRAP_PYTHON"
25+
OFF
26+
)
27+
2028
cmake_dependent_option(
2129
ITK_WRAP_unsigned_char
2230
"Wrap unsigned char type"

0 commit comments

Comments
 (0)