# Thread Safety Considerations for NSThread in MulleObjC

> Discover MulleObjC's unique NSThread thread safety rules, including thread affinity, cancellation flags, and join/detach patterns. Learn how these differ from Apple's Foundation.

- Repository: [mulle-objc/mulleobjc](https://github.com/mulle-objc/mulleobjc)
- Tags: thread-safety
- Published: 2026-03-07

---

**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`](https://github.com/mulle-objc/mulleobjc/blob/main/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`](https://github.com/mulle-objc/mulleobjc/blob/main/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`](https://github.com/mulle-objc/mulleobjc/blob/main/src/class/NSThread.h) (lines 89-99) stores objects keyed by UTF-8 strings, but enforces strict ownership:

- `MulleThreadSetObjectForKeyUTF8String()` stores values only if `mulleIsAccessibleByThread:` returns true
- `MulleThreadObjectForKeyUTF8String()` 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`](https://github.com/mulle-objc/mulleobjc/blob/main/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`](https://github.com/mulle-objc/mulleobjc/blob/main/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 `-mulleStart` followed by `-mulleJoin` for controllable lifecycles
- `-mulleJoin` blocks until completion and returns the original `NSInvocation`
- 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`](https://github.com/mulle-objc/mulleobjc/blob/main/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`](https://github.com/mulle-objc/mulleobjc/blob/main/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`](https://github.com/mulle-objc/mulleobjc/blob/main/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

```objc
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

```objc
@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

```objc
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 `NSThread` wraps exactly one native `mulle_thread` that 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 `-cancel` and `-isCancelled` flags; threads must poll and exit voluntarily.
- **TAO enforcement**: Enable `MulleObjCSetTAOFailureHandler` during development to catch cross-thread access violations immediately.
- **Thread-local storage**: Use `MulleThreadSetObjectForKeyUTF8String` and `MulleThreadObjectForKeyUTF8String` for 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`](https://github.com/mulle-objc/mulleobjc/blob/main/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.