Reading a value from a control from another thread

Hi!

I am writing an application when I need to read a value from a TextCtrl object from the thread other than wx Event Loop thread. Can I use the TextCtrl.GetValue() method safely in this case?
Making this question more general: Is calling any object method that only reads its value without changing state thread safe?
For changing state methods I use CallAfter but it does not return value from executed callback.
If getting value is not thread safe so what a trick I should use to make it thread safe?
Thank you guys in advance for your help!

what’s your urge for another thread ? :woozy_face:

My application is a bit complicated. I need to process some data without blocking GUI and notify GUI about some of the results of this processing.

I found an article on the blog where there is a note that only three methods are thread safe in wxPython:

  • wx.PostEvent
  • wx.CallAfter
  • wx.CallLater
    If it is true, I found the answer but will be nice if somebody also confirm that.
    To get the result from the callback I can use wx.CallLater.

well, you found a lot of material & if you are doubting thread safety of wxPython there is still the possibility of a python queue for data transfer…

Hi hazo,

This post (What is Thread Safety as applied to Python? - Users - Discussions on Python.org) and the explanations were nice to me and I think good for you.

The safest/cheapest way for long-run calculation is, never to use more than two threads! :slight_smile:

Getting the value of an object from another thread should not cause any problems (unless the widget is dead). However, in my experience, setting the value from outside the main thread could cause problems (a long time ago, I can’t reproduce the case). In that case, the CallAfter is sometimes helpful.

Note that wx.CallAfter posts callable object to the app and execute it in main thread when the app is idle. Note also that wx.CallLater is a wrapper class of timer, and you cannot call it from worker threads because the timer can only be started from the main thread.

If you want to call it from the threads, you have to write:

wx.CallAfter(
    wx.CallLater, 1000, callableObj, *args, **kwags)

If you want to know more about what CallAfter and CallLater are doing, it’s best to read the source code wx.core.py:3886~.

because of

CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously

Hi da-dada,
You mentioned GIL, but it is about low-level, c++ code execution.
If you have long-running calculations, you need to create at least one worker thread to prevent the GUI from freezing. You can make many threads, but as you know, it doesn’t help speed up the CPU-bound calculation at all due to GIL.

@hazo, Python’s built-in functions are thread-safe. wxPython’s built-in functions too (maybe… almost), although I remember a few unsafe cases e.g., calling wx.aui pane-> Show from outside the main thread causes the window to be lost.

The thread-safety of your program is on your own. I found a good article:

when Microsoft sends the updates down & the Windows Module Installer Worker takes over, the responsiveness of my laptop gets so flaming low that I regularly go for a walk (no Python or wxPython involved here, I think) :sneezing_face:

Thank you guys for all your responses. I want to make some summary of this topic:

  1. I have not found in wxPython documentation, that calling a control’s method that does not change control’s state is thread safe. As was stated before, we can assume that this is thread safe and does not lead to any problem if:
    1. control is alive
    2. method does not change the state of other controls
  2. There is no possibility of a race condition during execution of such a method call - Python will not release the GIL when running even complex wxPython C code.
  3. Following methods are for sure thread safe in wxPython: wx.PostEvent, wx.CallAfter, wx.CallLater, IsMainThread
  4. In cases when you are not completely sure that a method can be safely called from another thread you can use the following helper function. I have written it to be thread safe and return the result of called object.
def call_thread_safe(callable_obj: Callable, *args, **kwargs) -> Any:
    """
    Safely call the callable object from any thread.
    When calling from a GUI thread the object will be called directly.
    When calling from a non-GUI thread the object will be called
    after the current and pending GUI event handlers have been completed.

    :param callable_obj: the callable object
    :param args: arguments to be passed to the callable object
    :param kwargs: keywords to be passed to the callable object
    :return: result of calling the callable object
    """
    assert callable(callable_obj), 'callable_obj is not callable!'

    if wx.IsMainThread():
        return callable_obj(*args, **kwargs)
    else:
        event = threading.Event()
        result, exception, traceback = None, None, None

        def invoker():
            nonlocal result, exception, traceback
            try:
                result = callable_obj(*args, **kwargs)
            except:  # noqa
                _, exception, traceback = sys.exc_info()
            finally:
                event.set()

        wx.CallAfter(invoker)
        event.wait()
        if exception is None:
            return result
        else:
            raise type(exception)(exception).with_traceback(traceback)

call_thread_safe can be used from any thread including the MainLoop thread. Exceptions raised in callable_obj are re-raised.

If you have guys any comments, suggestions, improvements to this function, feel free to post them here.

wx.PostEvent (used also by Call…) uses AddPending and the description doesn’t sound all that safe to me :roll_eyes:

Thank you for the summary and good code. But I would like to point out one critical point.
The call_thread_safe calls CallAfter, then runs callable_obj in the main-thread. As a result, the GUI will freeze if the callable_obj is a CPU time-consuming function.

It surely is. Wx.CallAfter uses PostEvent and can be used from threads. But actually, wx.CallAfter posts wx.PyEvent (pure python object) instead of wx.Event, so I understood that it works.
From the docs, QueueEvent is one of the options, but it’s asynchronous and can’t be used if synchronization is important …

EDIT Thanks da-dada, I misread the document!

and in PyEvent (has wx._core.Event in its __mro__) especially the Clone method is the magic: not only transports it the parms hopefully correct, if it’s overwritten the deletion of the C++ object straight after handling will surface…

just like PostEvent (I personally think quite a lot of this ‘convenience’ stuff is confusing: kiss for ever :cowboy_hat_face:)