Using Python multithreading in 3D Slicer

Dear forum,

I am using Python to write an extension module in 3D Slicer. I need sub threads to perform heavy calculations while also interacting with the UI for some data. I used Python’s threading for multithreading programming, but there were some issues.

When I clicked the push button, the child thread started, but only ran a little before stopping. The child thread can only continue working when I slide the mouse over the image window. This makes me very confused. How can I solve this problem?

    self.ui.pushButton.clicked.connect(self.testButton)
    def testButton(self):
        self.ui.pushButton.setEnabled(False)
        self.thread1 = threading.Thread(target=self.execute)
        self.thread1.start()

    def execute(self):
        count = 10000000
        while count > 0:  # Simulate time-consuming operations
            if count % 100000 == 0:
                print(count)
            count = count-1
        print("Finished")
        self.ui.pushButton.setEnabled(True)

I have also read some posts and learned that CLI module or QProcess may be a better solution, but currently I still want to use multithreading to solve it.

Thanks for your help!

If you use threading in Python with threading, those will also run on the main thread, so it will not be executed on a different core. At least as far as I know.

I suggest you take a look at GitHub - pieper/SlicerParallelProcessing: Slicer modules for running subprocesses to operate on data in parallel
We successfully use it in a project so although @pieper says it is still experimental, I can confirm that it works.

1 Like

Yes, multithreading python is complicated by the Global Interpreter Lock (GIL). As @cpinter mentioned, the SlicerParallelProcessing extension is set up so you can transfer data with an instance of Slicer’s python environment running in a different process space. You can access all the python packages you have installed in you PythonSlicer environment, but you cannot access the GUI or MRML data, only the data you explicitly pass to the subprocess.

1 Like

@pieper @cpinter Sorry for the late reply. Thank you both for your suggestions! I will start learning parallel processing. However, I still don’t understand this sentence enough. If I want to access GUI or MRML data in subprocess, what should I do?

Thank you again for your response and contribution to the community.

When you move the mouse over the image window then you yield the GIL, this way all Python threads get a chance to run for a little. You can automate this regular yielding as it is done in the SimpleFilters module, by calling Python sleep() function regularly using a QTimer.

This technique allows keeping the GUI responsive while some computation is running in the background, but may not improve performance. See some more information about this here.

If you want to improve performance (not just GUI responsiveness):

  • If the code in the background thread does not release the GIL (e.g., pure Python code that operates on Python objects) then execution will not be faster by introducing more threads. You can run the processing code in a different process, for example using ParallelProcessing Slicer extension.
  • If the code in the background thread releases the GIL for significant time periods then you get true parallelism: your code may use multiple CPU cores. For example, SimpleITK releases the GIL, so if you use SimpleITK filters in your background thread then you will get not just responsive GUI but also processing code running in parallel with Python code. VTK does not release the GIL in Slicer (it would require setting a hidden VTK_PYTHON_FULL_THREADSAFE CMake flag to do so - see here).

@pieper @cpinter @jcfr If we want to improve Python threading support in Slicer, then we could consider:

  • yielding the Python GIL using a timer (so that Python threads just work, without each developer having to set up this mechanism)
  • build VTK with VTK_PYTHON_FULL_THREADSAFE=ON (to allow VTK to run in background Python threads without blocking Python code execution on other threads)
1 Like

Thank you very much for your reply! I think using QTimer may be useful, and I will give it a try later. In addition, I will also try to learn ParallelProcessing to see which method is more suitable for me. Because I have just come into contact with Slicer, I am not very familiar with these. Thank you again for your help!

Are you aware of any drawback related to enabling this flag? It seems a quick and clean way to improve multi-threading if it does not cause other issues. At least much of the Python processing code I have written has VTK calls all over the place, so it seems to be quite relevant.

The GIL is a mutex, so there is an extra cost in each VTK Python call because of its unlocking and locking. In most cases it is probably insignificant, as Python is very slow anyway. But we should stress test this a bit to make sure we don’t introduce any perceivable slowdown in normal single-threaded operation. We could ask ParaView/VTK developers about this, too, as they have more experience with this.

@Hunger-beat a lot depends on what you are doing in the threads. This is a longstanding issue. As you can see on this page SimpleITK GIL support was added in 2013, using methods discussed on the VTK mailing list in 2005. Using a thread and releasing the GIL when calling long-running C or C++ code makes sense since it can run independent of Python. But it doesn’t make sense if the code is executing Python or interacting with the GUI, which are single threaded.

I’ve never tried that, but it might be okay. I agree we’d need to check if it introduces subtle bugs or performance issues.

Okay, I understand. I will study carefully. Thank you again for your answers!

Did you implement improve “Python threading support in Slicer” already?

There has not been much interest in this since this discussion last year (perhaps because threading in Python does not actually mean parallel execution, so not really impactful), so we did not make any changes. You can do the same what is done in SimpleFilters module (calling Python sleep() function regularly using a QTimer - see details above) and let us know if you find it useful.