The relevant code is in objc-sync.m
Synchronizing is implemented in terms in pthread_mutexes. A pthread_mutex is a reentrant mutex which may only be acquired by one thread at a time. Locks must be balanced by unlocks. If locked, other threads will block until it is unlocked.
Each pthread has an array of active locks (this array is only created if needed). In the best case ( id2data part 1), the current thread already has the object locked, so the mutex is located there and there are no multithreading/race conditions because the object is already locked by the current thread. Releasing is always best case unless there's a compiler error or memory corruption.
In the worst case ( id2data part 2), a spinlock is used while a hashtable/linked list is scanned for the object/mutex pair. If not found, the linked list is scanned for a free mutex. If no free mutex is available, a new one is allocated.
The runtime keeps an additional lockCount (the number of times an object/mutex is locked by this thread) and threadCount (the number of threads interested in this object/mutex). When the lockCount drops to 0, the entry is removed from the thread's array and the threadCount is decremented. Whent he threadCount drops to 0, it is left in the hashtable/linked list, but eligible for reuse by another object with the same hash. There isn't a separate pool of unused entries and memory or mutexes are never deallocated.
typedef struct {
spin_lock_t lock;
SyncData *data;
} SyncList __attribute__((aligned(64)));
// aligned to put locks on separate cache lines
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
typedef struct SyncData {
struct SyncData* nextData;
id object;
int threadCount; // number of THREADS using this block
pthread_mutex_t mutex;
pthread_cond_t conditionVariable;
} SyncData;
// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];
// Begin synchronizing on 'obj'.
// Allocates recursive pthread_mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
result = pthread_mutex_lock(&data->mutex);
require_noerr_string(result, done, "pthread_mutex_lock failed");
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
result = objc_sync_nil();
}
done:
return result;
}
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
result = pthread_mutex_unlock(&data->mutex);
require_noerr_string(result, done, "pthread_mutex_unlock failed");
} else {
// @synchronized(nil) does nothing
}
done:
if ( result == EPERM )
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
return result;
}
static SyncData* id2data(id object, enum usage why)
{
spin_lock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
int err;
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
int i;
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
result = item->data;
require_action_string(result->threadCount > 0, cache_done,
result = NULL, "id2data cache is buggy");
require_action_string(item->lockCount > 0, cache_done,
result = NULL, "id2data cache is buggy");
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 0) {
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
cache_done:
return result;
}
}
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
_spin_lock(lockp);
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = object;
result->threadCount = 1;
goto done;
}
// malloc a new SyncData and add to list.
// XXX calling malloc with a global lock held is bad practice,
// might be worth releasing the lock, mallocing, and searching again.
// But since we never free these guys we won't be stuck in malloc very often.
result = (SyncData*)malloc(sizeof(SyncData));
result->object = object;
result->threadCount = 1;
err = pthread_mutex_init(&result->mutex, recursiveAttributes());
require_noerr_string(err, done, "pthread_mutex_init failed");
err = pthread_cond_init(&result->conditionVariable, NULL);
require_noerr_string(err, done, "pthread_cond_init failed");
result->nextData = *listp;
*listp = result;
done:
_spin_unlock(lockp);
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread cache above.
require_string(result != NULL, really_done, "id2data is buggy");
require_action_string(why == ACQUIRE, really_done,
result = NULL, "id2data is buggy");
require_action_string(result->object == object, really_done,
result = NULL, "id2data is buggy");
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used++] = (SyncCacheItem){result, 1};
}
really_done:
return result;
}