Is the objc_msgSend function thread-safe?
Solution 1:
The details of this answer depend on the specifics of what you mean by "thread-safe", but for most definitions of "thread-safe" (e.g., calling this code from multiple threads will leave your program in a consistent state without unintended interactions), then the answer is yes, objc_msgSend
is thread-safe.
The specifics of how this is done are complex, but some context:
- A function is inherently thread-safe if it does not modify state shared across threads in a way that could lead to unintended consequences
-
For example,
func add(_ a: Int, _ b: Int) -> Int { a + b }
is inherently thread-safe — you can't call it in a way that would cause it to mutate shared state somehow
-
- A function is thread-safe if it does modify state shared across threads, but in a way that is coordinated with other threads reading that state
objc_msgSend
falls into the latter camp: there are cases in which it does need to modify shared state (entries in the method cache), but it does so in a coordinated way.
Some helpful references, written by Mike Ash (who last I knew, was still working on the Obj-C runtime at Apple):
- Friday Q&A 2017-06-30: Dissecting objc_msgSend on ARM64
- Friday Q&A 2015-05-29: Concurrent Memory Deallocation in the Objective-C Runtime
- objc_msgSend's New Prototype
In "Dissecting objc_msgSend
on ARM64", Mike goes through the specifics of how objc_msgSend
works, line-by-line. While the code has changed a bit since 2017, the specifics are all largely the same:
- It gets the class of the target object passed in
- It finds the method cache for that class
- It uses the selector called on the target object to look up a method implementation for that method in the cache
- If a method is not found in the cache, it falls back to looking it up, and possibly inserts it into the cache
- It calls the implementation of that method directly
Of these operations, (1), (2), and (4) are most obviously thread-safe: (1) and (4) are inherently thread-safe (and don't rely on other threads at all), and (2) performs an atomic read that is safe across threads.
(3) is the trickier operation, since the method cache is shared for all instances of a class, and if you're calling a method on multiple instances of the same class at the same time, they might be digging through the method cache simultaneously. "Concurrent Memory Deallocation in the Objective-C Runtime" goes into this a little bit more on how this is done, but the general gist is that there are plenty of cool synchronization tricks that you can without needing to lock across threads. The full explanation is out of scope (and the article goes into a lot of detail anyway), but in a comment on "Dissecting objc_msgSend
on ARM64" from Blaine Garst (one of the original authors of many of the features you might recognize as part of the Objective-C runtime):
Mike, you missed explaining the greatest secret of the messenger, one that Matt and Dave and I cooked up on the black couches at Seaport: how it is that multiple clients can read a mutable hash table without synchronization. It stumps people to this day. The answer is that the messenger uses a specialized garbage collector! I had put locks in when we first tried to do multi-threaded ObjC and it was miserable and then I read a paper by David Black where the kernel would reset the PC back to the start of a critical range for a mutex, and, well, we use the absense of a PC in critical ranges to deduce safety of reclaiming stale caches.
So, this operation is coordinated across threads too.
All of the work that objc_msgSend
does is safe across thread boundaries, so much so that you can call methods on the same exact object across any number of threads at the exact same time, and it will work just fine. (Whether the implementation of the method is thread-safe is something else entirely.)
And in the end, it has to be. objc_msgSend
is the backbone of pretty much all of Objective-C. If it wasn't thread-safe, it would be impossible to write multi-threaded Objective-C code. But luckily it is, and you can rely on it working at scale.