Thread Safety Considerations for NSThread in MulleObjC
NSThread in MulleObjC requires strict adherence to thread-affinity rules, atomic cancellation flags, and explicit join/detach patterns that differ significantly from Apple's Foundation implementation.
The mulle-objc/mulleobjc repository provides a custom Objective-C runtime with its own threading model built on top of the low-level mulle_thread primitive. Unlike Apple's Foundation, MulleObjC enforces thread-affinity-only (TAO) access patterns and exposes explicit lifecycle controls that prevent common concurrency errors.
One-to-One Thread Mapping
Each NSThread instance maintains a strict one-to-one relationship with a native operating system thread. In src/class/NSThread.h (lines 50-56), the _osThread member stores the underlying mulle_thread handle, and the object is automatically destroyed when the native thread terminates.
This tight coupling means you cannot reuse or switch NSThread contexts. The object always represents exactly one OS thread from creation to termination. Attempting to manipulate the _osThread pointer directly violates the thread-safety guarantees and leads to undefined behavior.
Thread-Unsafe Argument Passing
When spawning threads using selector-based APIs or function pointers, MulleObjC retains all arguments passed to the new thread, but does not make them thread-safe. According to the source in src/class/NSThread.h (lines 52-56), after the thread starts, those objects become exclusively owned by the new thread and must not be accessed by the creator thread or any other context.
This applies to:
- Selector/target pairs using
detachNewThreadSelector:toTarget:withObject: - Function pointers passed to
mulleDetachNewThreadWithFunction:argument: - NSInvocation objects used as entry points
Violating this rule by touching the payload after thread creation results in race conditions under the TAO (Thread-Affinity-Only) model.
Thread-Local Storage and Affinity
MulleObjC replaces Foundation's threadDictionary with a lightweight per-thread map accessible through C helper functions. The _map field in src/class/NSThread.h (lines 89-99) stores objects keyed by UTF-8 strings, but enforces strict ownership:
MulleThreadSetObjectForKeyUTF8String()stores values only ifmulleIsAccessibleByThread:returns trueMulleThreadObjectForKeyUTF8String()retrieves thread-local data without cross-thread contamination
The runtime includes assertions that verify assert([value mulleIsAccessibleByThread:threadObject]) before allowing storage operations, preventing accidental sharing of non-thread-safe objects between threads.
Cooperative Cancellation and Lifecycle
Cancellation in MulleObjC is cooperative rather than forced. The _cancelled flag (lines 84-86 in src/class/NSThread.h) is stored atomically and checked by the thread's own execution code. You signal cancellation via -cancel and query status via -isCancelled, but the thread must poll and exit manually.
For lifecycle control, MulleObjC provides two distinct patterns in src/class/NSThread.h (lines 60-70):
Detached Threads
- Use
+mulleDetachNewThreadWithFunction:argument:for fire-and-forget execution - No join possible; thread cleans up automatically on exit
- Suitable for background tasks requiring no result
Joinable Threads
- Use
-mulleStartfollowed by-mulleJoinfor controllable lifecycles -mulleJoinblocks until completion and returns the originalNSInvocation- Releases thread resources after joining, preventing leaks
Main-Thread Guarantees and Safe Accessors
MulleObjC provides explicit runtime state inspection via +mulleIsMainThread and +mulleIsMultiThreaded (lines 82-88 in src/class/NSThread.h). These reflect actual runtime state rather than cached properties, offering reliable main-thread detection.
All public accessors exposing internal state are marked with MULLE_OBJC_THREADSAFE_METHOD or MULLE_OBJC_THREADSAFE_PROPERTY. The underlying atomic pointers—including _osThread, _runLoop, _nameUTF8String, and _cancelled—guarantee lock-free reads and writes across threads (lines 58-64 in src/class/NSThread.h).
Code requiring main-thread execution should use the MULLE_OBJC_MAINTHREAD_METHOD annotation, which expands to MULLE_OBJC_THREADSAFE_METHOD and documents thread requirements.
Debugging with TAO Failure Detection
To catch thread-affinity violations during development, MulleObjC supports a custom failure handler configurable via MulleObjCSetTAOFailureHandler (lines 125-138 in src/class/NSThread.h). When enabled, this handler aborts the process immediately if code attempts to access an object from a thread other than its owning thread.
This mechanism is invaluable for debugging TAO violations during the development phase, though it should be disabled or replaced with logging in production environments.
Code Examples
Detached Thread with Function Pointer
static int MyThreadFunc(NSThread *thread, void *arg)
{
NSLog(@"Running on thread %@, arg = %s", thread, (char *)arg);
// Store thread-local data accessible only to this thread
MulleThreadSetObjectForKeyUTF8String(@(42), "answer");
return 0;
}
// Launch detached thread - cannot be joined later
[NSThread mulleDetachNewThreadWithFunction:MyThreadFunc
argument:"hello"];
Selector-Based Thread with Unsafe Arguments
@interface Worker : NSObject
- (void)doWork:(id)payload;
@end
@implementation Worker
- (void)doWork:(id)payload
{
// payload is owned exclusively by this thread
NSLog(@"Worker received %@", payload);
}
@end
// Arguments retained for new thread, must not be touched afterward
[NSThread detachNewThreadSelector:@selector(doWork:)
toTarget:[Worker new]
withObject:@{ @"key": @"value" }];
Joinable Thread Pattern
NSThread *t = [[NSThread alloc] initWithTarget:nil
selector:nil
object:nil];
[t mulleInitWithFunction:MyThreadFunc argument:(void *)"joined"];
[t mulleStart]; // Thread begins execution
[t mulleJoin]; // Wait for completion, releases resources
[t release];
Summary
- Strict 1:1 mapping: Each
NSThreadwraps exactly one nativemulle_threadthat cannot be reused or switched. - Argument isolation: Objects passed to new threads are retained but not thread-safed; never access them after starting the thread.
- Explicit lifecycle: Choose between detached (
mulleDetachNewThread…) and joinable (mulleStart/mulleJoin) patterns based on synchronization needs. - Cooperative cancellation: Use atomic
-canceland-isCancelledflags; threads must poll and exit voluntarily. - TAO enforcement: Enable
MulleObjCSetTAOFailureHandlerduring development to catch cross-thread access violations immediately. - Thread-local storage: Use
MulleThreadSetObjectForKeyUTF8StringandMulleThreadObjectForKeyUTF8Stringfor safe per-thread data.
Frequently Asked Questions
How does MulleObjC NSThread differ from Apple's Foundation implementation?
MulleObjC's NSThread is built directly on the mulle_thread primitive rather than POSIX threads or Cocoa's abstraction layer. It enforces thread-affinity-only (TAO) access patterns, requires explicit join/detach lifecycle management, and lacks true threadDictionary in favor of UTF-8 keyed thread-local maps. The runtime asserts object accessibility rather than assuming Foundation's broader thread-safety model.
Can I share objects between threads in MulleObjC?
Only if those objects are explicitly designed for thread-sharing. The TAO model requires that most objects be accessed only by their creating thread. Use MulleThreadSetObjectForKeyUTF8String for thread-local storage, or ensure objects implement proper synchronization primitives. The runtime can be configured to abort on TAO violations via MulleObjCSetTAOFailureHandler.
Why can't I access arguments after starting a detached thread?
MulleObjC retains arguments for the new thread's use but marks them as owned by that specific thread. According to src/class/NSThread.h (lines 52-56), accessing these objects from the original thread after launch violates thread-affinity rules and creates race conditions. Once passed, treat the arguments as transferred to the new thread's exclusive ownership.
How do I properly wait for a MulleObjC thread to finish?
Use the joinable pattern: call -mulleStart to begin execution, then -mulleJoin to block until completion. Unlike Foundation's NSThread, which only supports detachment, MulleObjC's join mechanism returns the NSInvocation and releases thread resources. Do not use -mulleJoin on threads created via +mulleDetachNewThread…, as detached threads cannot be joined.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →