Repository: numba/numba

Issue:
parallel vectorize deadlock when reacquiring the GIL
Using `print()` or ctypes callback (CFUNCTYPE) with in the ufunc kernel of a parallel ufunc causing deadlock.  This is due to a parallel ufunc launcher not releasing the GIL.  I have a fix and will be uploading the patch soon.

From mailing list: https://groups.google.com/a/continuum.io/forum/?utm_medium=email&utm_source=footer#!msg/numba-users/zxoG6maTnU8/_udU5q5_AwAJ



Discussion:
Actually, numpy ufunc machinery may release the GIL is the input array is big enough.  The patch needs to check if the GIL is released already.


Resolving patch:
diff --git a/numba/npyufunc/parallel.py b/numba/npyufunc/parallel.py
--- a/numba/npyufunc/parallel.py
+++ b/numba/npyufunc/parallel.py
@@ -112,6 +112,13 @@ def build_ufunc_kernel(library, ctx, innerfunc, sig):
 
     args, dimensions, steps, data = lfunc.args
 
+    # Release the GIL (and ensure we have the GIL)
+    # Note: numpy ufunc may not always release the GIL; thus,
+    #       we need to ensure we have the GIL.
+    pyapi = ctx.get_python_api(builder)
+    gil_state = pyapi.gil_ensure()
+    thread_state = pyapi.save_thread()
+
     # Distribute work
     total = builder.load(dimensions)
     ncpu = lc.Constant.int(total.type, NUM_THREADS)
@@ -181,8 +188,14 @@ def build_ufunc_kernel(library, ctx, innerfunc, sig):
 
     # Signal worker that we are ready
     builder.call(ready, ())
+
     # Wait for workers
     builder.call(synchronize, ())
+
+    # Work is done. Reacquire the GIL
+    pyapi.restore_thread(thread_state)
+    pyapi.gil_release(gil_state)
+
     builder.ret_void()
 
     return lfunc
diff --git a/numba/utils.py b/numba/utils.py
--- a/numba/utils.py
+++ b/numba/utils.py
@@ -14,6 +14,12 @@
 import numpy as np
 
 from .six import *
+try:
+    # preferred over pure-python StringIO due to threadsafety
+    # note: parallel write to StringIO could cause data to go missing
+    from cStringIO import StringIO
+except ImportError:
+    from io import StringIO
 from numba.config import PYVERSION, MACHINE_BITS
 
 


Test patch:
diff --git a/numba/tests/npyufunc/test_parallel_ufunc_issues.py b/numba/tests/npyufunc/test_parallel_ufunc_issues.py
--- a/numba/tests/npyufunc/test_parallel_ufunc_issues.py
+++ b/numba/tests/npyufunc/test_parallel_ufunc_issues.py
@@ -1,9 +1,14 @@
 from __future__ import print_function, absolute_import, division
 
+import time
+import ctypes
+
+import numpy as np
+
 from numba import unittest_support as unittest
+from numba.tests.support import captured_stdout
 from numba import vectorize
-import numpy as np
-import time
+
 
 class TestParUfuncIssues(unittest.TestCase):
     def test_thread_response(self):
@@ -26,6 +31,48 @@ def fnv(a, b):
             # Reduce sleep time
             sleep_time /= 2
 
+    def test_gil_reacquire_deadlock(self):
+        """
+        Testing issue #1998 due to GIL reacquiring
+        """
+        # make a ctypes callback that requires the GIL
+        proto = ctypes.CFUNCTYPE(None, ctypes.c_int32)
+        characters = 'abcdefghij'
+
+        def bar(x):
+            print(characters[x])
+
+        cbar = proto(bar)
+
+        # our unit under test
+        @vectorize(['int32(int32)'], target='parallel', nopython=True)
+        def foo(x):
+            print(x % 10)  # this reacquires the GIL
+            cbar(x % 10)   # this reacquires the GIL
+            return x * 2
+
+        # Numpy ufunc has a heuristic to determine whether to release the GIL
+        # during execution.  Small input size (10) seems to not release the GIL.
+        # Large input size (1000) seems to release the GIL.
+        for nelem in [1, 10, 100, 1000]:
+            # inputs
+            a = np.arange(nelem, dtype=np.int32)
+            acopy = a.copy()
+            # run and capture stdout
+            with captured_stdout() as buf:
+                got = foo(a)
+            stdout = buf.getvalue()
+            buf.close()
+            # process outputs from print
+            got_output = sorted(map(lambda x: x.strip(), stdout.splitlines()))
+            # build expected output
+            expected_output = [str(x % 10) for x in range(nelem)]
+            expected_output += [characters[x % 10] for x in range(nelem)]
+            expected_output = sorted(expected_output)
+            # verify
+            self.assertEqual(got_output, expected_output)
+            np.testing.assert_equal(got, 2 * acopy)
+
 
 if __name__ == '__main__':
     unittest.main()