You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: macosx backend crashes when used on a worker thread #31968
While Interactive figures and asynchronous programming specifically states that "In general Matplotlib is not thread safe", the macosx backend crashes with SIGSEGV or SIGTRAP when various methods are called on a worker thread:
Method
Result
FigureCanvas.flush_events
CRASH
FigureManager.set_window_title
Obj-C Exception
FigureManager.show
CRASH
FigureManager.resize
CRASH
FigureManager.full_screen_toggle
CRASH
Some methods, like FigureCanvas.set_cursor, do work without crashing; however, NSCursor isn't documented to be thread-safe. It's also not on the historic list of thread-unsafe classes, so our usage falls into a gray area.
I think we should default to setting a Python error when any macosx backend method is called on a background thread, with specific exceptions for Timer or FigureCanvas.draw_idle(). Possibly others?
I'm not convinced we should scatter thread checks all over the backends. This is just not supported. Code is generally not thread-safe and when working with threads, users have to explicitly check whether this is supported. Therefore, I don't think we have the obligation to catch unsupported threaded calls.
Most GUI frameworks require that all updates to the screen, and hence their main event loop, run on the main thread.
We could be clearer here: "All supported GUI frameworks require ..." and add "When using any of the built-in GUI backends, all matplotlib code must run in the main thread".
Therefore, I don't think we have the obligation to catch unsupported threaded calls.
That's completely fair, and there are plenty of ways that someone can cause a crash from a Python script if they try hard enough :)
That said, for the macosx backend, we already need to set up an autorelease pool and exception handler prior to entering Obj-C land. We could add a main thread check to the BEGIN_OBJC_ENTRY macro if desired and get it for "free".
My main issue is that, internally, Apple does a main thread check and then uses _os_crash() with a "Must only be used from the main thread" message. That's great if you are running inside of Xcode and the _os_crash handler fires and logs - at least you get an idea where you messed up. It's not so great if you are running from terminal and get a cryptic zsh: trace trap python3 ~/Desktop/crash.py.
We could be clearer here: "All supported GUI frameworks require ..." and add "When using any of the built-in GUI backends, all matplotlib code must run in the main thread".
From the description of #25553, I thought that using a worker thread to update figures and then calling draw_idle() is specifically supported. Is this not the case?
I think that there are quite a few people that use thread pools to stream data and aggregate things from multiple sensors into the data objects we are plotting. I think what we want is for the GUI toolkit to do the drawing from a single main thread I believe? So is what we really need here is a dispatch_async() on the cases you identified as crashing? canvas.flush_events() would dispatch the flush to the main thread even if called from a worker thread. I'm not sure we care where the draw_idle() was called from, just that the GUI updates and work should be from the main thread.
I think that there are quite a few people that use thread pools to stream data and aggregate things
I'm going to reply in detail tomorrow (Saturday). For now: I 100% agree that this is something we should be aware of and support. I fully agree that draw_idle() should be callable on a worker thread and should immediately dispatch_async() to the main queue.
I think that in most other cases, calling dispatch_async() will change our API contract. For example, flush_events() is defined to "…run the GUI event loop until all UI events currently waiting have been processed". You can't do that from a worker thread without a dispatch_sync(), which would open up the possibility to deadlock.
The list in my original issue represents a subset of potential problems. The actual results may vary depending on macOS version, architecture, and race conditions.
Apple provides a tool called Main Thread Checker which "verifies that system APIs that must run on the main thread actually do run on that thread". It is enabled by default when you create an application in Xcode.
Due to macOS Security Restrictions, the best way for us to invoke it is via ctypes.CDLL:
Main Thread Checker Example
importthreadingimportmatplotlib.pyplotaspltimportosimportctypesimportsysimporttime# Uncomment to turn on verbose mode# This will dump a list of non-main-thread classes# os.environ["MTC_VERBOSE"] = "1"# Load AppKit first, then libMainThreadCheckerctypes.CDLL("/System/Library/Frameworks/AppKit.framework/AppKit")
ctypes.CDLL(
"/Applications/Xcode.app/Contents/Developer/usr/lib/libMainThreadChecker.dylib"
)
fig, ax=plt.subplots()
defworker():
print(fig.canvas.manager.get_window_title())
thread=threading.Thread(target=worker)
thread.start()
plt.show()
Running this example results in an error:
Main Thread Checker: UI API called on a
background thread: -[NSWindow title]
I spent the morning running similar examples on the old macosx backend. I tried to exercised every entry point into "_macosx.m". The results are as follows:
Main Thread Checker Results
Icon
Meaning
✅
No issue
⚠️
Main Thread Checker error
🛑
Crashed
FigureCanvas
Method
Result
update
⚠️
flush_events
🛑
set_cursor
✅
set_rubberband
⚠️
remove_rubberband
⚠️ (if rubberband set)
_start_event_loop
🛑
stop_event_loop
✅ (postEvent:atStart: on allowlist)
FigureManager
Method
Result
_show
🛑
_raise
🛑
_set_window_mode
⚠️
set_window_title
🛑
get_window_title
⚠️
resize
🛑
full_screen_toggle
🛑
NavigationToolbar2
Method
Result
set_message
⚠️
Timer
Method
Result
_timer_start
✅
_timer_stop
✅
_macosx
Method
Result
event_loop_is_running
✅
wake_on_fd_write
✅
stop
⚠️
show
🛑
choose_save_file
(fails with message to stderr)
Each "⚠️" is a potential crash. Even if the call works today, Apple makes no guarantee that it will continue to work in the future. I've been burned several times in my career where a minor or bugfix release breaks existing code (although they were generally my fault!)
I really don't want to add dispatch_async() logic for each ⚠️/🛑.
As mentioned yesterday, I don't know of a safe way to do this for methods like flush_events(), as it is suppose to block until no more events.
If we use dispatch_async() for set_window_title, we will almost certainly get a stale window title. We would need to:
Use dispatch_sync() in get_window_title and introduce a potential deadlock.
Store a copy of the title in the manager and access it in a @synchronized block. This works, but it's a lot of added complexity for what I would consider little utility to our users.
resize() or full_screen_toggle() could safety dispatch to the main thread, but again: what actual utility does it provide?
Where I think it makes sense
I definitely think that we need to support draw_idle() on a worker thread. I suspect that this is the main use.
I'm unsure about Timers. While they currently work in the old backend, I believe that your main intention in #27527 was focused on draw_idle() due to the internal implementation.
I'm not aware of many UI frameworks where a timer created/started on a worker thread will always fire its callback on the main thread. If the user wants this behavior, wouldn't they create/start it there?
Whether or not this even works on a background thread depends on the backend:
Backend
Result
macosx, gtk, tk
Works
qt, wx
Fails with error
If the user is using a single_shot Timer to mimic dispatch_async - that's an interesting use case. It fails on qt and wx, but there may be some utility there. I could see an argument for "please queue this lambda and execute it on the main thread's event loop". Does such a mechanism already exist in Python?
Overall, I still think that we should default to raising a Python error if we are about to cross into Objective-C with the exception of very-strategically-chosen methods (draw_idle()).
The code for this is simple. While it would also raise an error for Timers, this matches the existing behavior of qt and wx.
Please let me know if there are any real-world use cases other than draw_idle()!
I agree that the main desire is around draw_idle() for myself at least, so I would be fine with putting the guards in for the other methods, and a Python error is much better than a segfault.
I'm not aware of many UI frameworks where a timer created/started on a worker thread will always fire its callback on the main thread. If the user wants this behavior, wouldn't they create/start it there?
I think I agree with this and the code in the callback should be responsible for delegating out the work to the proper thread if needed. I am guessing that the reason some of this is implemented to send to the main thread is for the simplicity of avoiding needing to think about what thread the timer was created on, maybe not correct but simple. I'd be fine with doing the more correct thing, but I think we need to make sure our methods are delegating appropriately then like calling draw_idle() from a worker thread would make sure that the draw() work gets done on the main thread even if called from somewhere else.
To be clear, draw_idle() on a worker thread should absolutely invoke draw() on the main thread!
I pondered this a lot yesterday. Event loops, timers, and draw_idle() all have inconsistent behavior amongst the various interactive backends. You are addressing some of the timer concerns in #29062. I've identified a few others and will probably have an "Even more consistent timers" PR at some point in the future :) For event loops, I filed #31858 back in June.
I think that, ultimately, we should have consistent behavior across the backends. We should understand the specific use cases and try to address them in a way that is still maintainable.
For the new macOS backend, I think it makes the most sense to be strict/correct/simple from the very beginning while also addressing the most common cases (draw_idle(), maybe others). If this breaks an existing script in 3.12, the user can switch to the macosx backend and (hopefully) file an issue.
The one situation I'm reviewing right now is FigureCanvas.new_timer(). All existing backends allow the timer to be created on a worker thread; however, from that point onward the behavior is all over the place. For macos, The cleanest approach would be to throw an error in new_timer() and not allow the Timer to be created; however, maybe we should be consistent with the other backends and not throw until start()? That will complicate the native code, unfortunately.
Activity
timhoffm commentedon Jun 30, 2026
I'm not convinced we should scatter thread checks all over the backends. This is just not supported. Code is generally not thread-safe and when working with threads, users have to explicitly check whether this is supported. Therefore, I don't think we have the obligation to catch unsupported threaded calls.
Interactive figures and asynchronous programming also states:
We could be clearer here: "All supported GUI frameworks require ..." and add "When using any of the built-in GUI backends, all matplotlib code must run in the main thread".
iccir commentedon Jun 30, 2026
That's completely fair, and there are plenty of ways that someone can cause a crash from a Python script if they try hard enough :)
That said, for the macosx backend, we already need to set up an autorelease pool and exception handler prior to entering Obj-C land. We could add a main thread check to the
BEGIN_OBJC_ENTRYmacro if desired and get it for "free".My main issue is that, internally, Apple does a main thread check and then uses
_os_crash()with a "Must only be used from the main thread" message. That's great if you are running inside of Xcode and the_os_crashhandler fires and logs - at least you get an idea where you messed up. It's not so great if you are running from terminal and get a crypticzsh: trace trap python3 ~/Desktop/crash.py.From the description of #25553, I thought that using a worker thread to update figures and then calling
draw_idle()is specifically supported. Is this not the case?greglucas commentedon Sep 11, 2026
I think that there are quite a few people that use thread pools to stream data and aggregate things from multiple sensors into the data objects we are plotting. I think what we want is for the GUI toolkit to do the drawing from a single main thread I believe? So is what we really need here is a dispatch_async() on the cases you identified as crashing?
canvas.flush_events()would dispatch the flush to the main thread even if called from a worker thread. I'm not sure we care where the draw_idle() was called from, just that the GUI updates and work should be from the main thread.iccir commentedon Sep 12, 2026
I'm going to reply in detail tomorrow (Saturday). For now: I 100% agree that this is something we should be aware of and support. I fully agree that
draw_idle()should be callable on a worker thread and should immediatelydispatch_async()to the main queue.I think that in most other cases, calling
dispatch_async()will change our API contract. For example,flush_events()is defined to "…run the GUI event loop until all UI events currently waiting have been processed". You can't do that from a worker thread without adispatch_sync(), which would open up the possibility to deadlock.iccir commentedon Sep 13, 2026
The list in my original issue represents a subset of potential problems. The actual results may vary depending on macOS version, architecture, and race conditions.
Apple provides a tool called Main Thread Checker which "verifies that system APIs that must run on the main thread actually do run on that thread". It is enabled by default when you create an application in Xcode.
Due to macOS Security Restrictions, the best way for us to invoke it is via
ctypes.CDLL:Main Thread Checker Example
Running this example results in an error:
I spent the morning running similar examples on the old macosx backend. I tried to exercised every entry point into "_macosx.m". The results are as follows:
Main Thread Checker Results
FigureCanvas
(if rubberband set)
(
postEvent:atStart:on allowlist)FigureManager
NavigationToolbar2
Timer
_macosx
Each "⚠️ " is a potential crash. Even if the call works today, Apple makes no guarantee that it will continue to work in the future. I've been burned several times in my career where a minor or bugfix release breaks existing code (although they were generally my fault!)
I really don't want to add⚠️ /🛑.
dispatch_async()logic for eachAs mentioned yesterday, I don't know of a safe way to do this for methods like
flush_events(), as it is suppose to block until no more events.There are also cases like the following:
If we use
dispatch_async()forset_window_title, we will almost certainly get a stale window title. We would need to:dispatch_sync()inget_window_titleand introduce a potential deadlock.@synchronizedblock. This works, but it's a lot of added complexity for what I would consider little utility to our users.resize()orfull_screen_toggle()could safety dispatch to the main thread, but again: what actual utility does it provide?Where I think it makes sense
I definitely think that we need to support
draw_idle()on a worker thread. I suspect that this is the main use.I'm unsure about Timers. While they currently work in the old backend, I believe that your main intention in #27527 was focused on
draw_idle()due to the internal implementation.I'm not aware of many UI frameworks where a timer created/started on a worker thread will always fire its callback on the main thread. If the user wants this behavior, wouldn't they create/start it there?
Whether or not this even works on a background thread depends on the backend:
If the user is using a
single_shotTimer to mimicdispatch_async- that's an interesting use case. It fails on qt and wx, but there may be some utility there. I could see an argument for "please queue this lambda and execute it on the main thread's event loop". Does such a mechanism already exist in Python?Overall, I still think that we should default to raising a Python error if we are about to cross into Objective-C with the exception of very-strategically-chosen methods (
draw_idle()).The code for this is simple. While it would also raise an error for Timers, this matches the existing behavior of qt and wx.
Please let me know if there are any real-world use cases other than
draw_idle()!greglucas commentedon Sep 14, 2026
I agree that the main desire is around
draw_idle()for myself at least, so I would be fine with putting the guards in for the other methods, and a Python error is much better than a segfault.I think I agree with this and the code in the callback should be responsible for delegating out the work to the proper thread if needed. I am guessing that the reason some of this is implemented to send to the main thread is for the simplicity of avoiding needing to think about what thread the timer was created on, maybe not correct but simple. I'd be fine with doing the more correct thing, but I think we need to make sure our methods are delegating appropriately then like calling
draw_idle()from a worker thread would make sure that thedraw()work gets done on the main thread even if called from somewhere else.iccir commentedon Sep 14, 2026
To be clear,
draw_idle()on a worker thread should absolutely invokedraw()on the main thread!I pondered this a lot yesterday. Event loops, timers, and
draw_idle()all have inconsistent behavior amongst the various interactive backends. You are addressing some of the timer concerns in #29062. I've identified a few others and will probably have an "Even more consistent timers" PR at some point in the future :) For event loops, I filed #31858 back in June.I think that, ultimately, we should have consistent behavior across the backends. We should understand the specific use cases and try to address them in a way that is still maintainable.
For the new macOS backend, I think it makes the most sense to be strict/correct/simple from the very beginning while also addressing the most common cases (
draw_idle(), maybe others). If this breaks an existing script in 3.12, the user can switch to themacosxbackend and (hopefully) file an issue.The one situation I'm reviewing right now is
FigureCanvas.new_timer(). All existing backends allow the timer to be created on a worker thread; however, from that point onward the behavior is all over the place. Formacos, The cleanest approach would be to throw an error innew_timer()and not allow the Timer to be created; however, maybe we should be consistent with the other backends and not throw untilstart()? That will complicate the native code, unfortunately.