Kuro5hin.org: technology and culture, from the trenches
create account | help/FAQ | contact | links | search | IRC | site news
[ Everything | Diaries | Technology | Science | Culture | Politics | Media | News | Internet | Op-Ed | Fiction | Meta | MLP ]
We need your support: buy an ad | premium membership

[P]
@synchronized() : under the hood

By horny smurf in horny smurf's Diary
Fri Jan 21, 2011 at 07:39:21 PM EST
Tags: os x, cocoa, objective c, programming, necrophilia, horse cock (all tags)

Objective C 2.0 introduced @synchronized blocks (much like java's synchronized blocks) to make multithreaded programming easier.

How does it work? Good question, I'm glad you asked.


-(void)bleh
{
    @synchronized(self)
    {
        // code ...
    }
}

is rewritten as:


-(void)blow
{
    objc_sync_enter(self)
    @try
    {
        // code ...
    }
    @finally
    {
        objc_sync_exit(self);
    }
}

(I'll discuss @try/@throw/@catch/@finally next time).


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;
}

Sponsors

Voxel dot net
o Managed Hosting
o VoxCAST Content Delivery
o Raw Infrastructure

Login

Related Links
o objc-sync. m
o reentrant mutex
o spinlock
o horny smurf's Diary


Display: Sort:
@synchronized() : under the hood | 4 comments (4 topical, editorial, 0 hidden)
When i was working apple (3.00 / 5) (#1)
by I Did It All For The Horse Cock on Fri Jan 21, 2011 at 07:56:00 PM EST

I was debugging this code and Jobs himself walked in as he known about this bug for a while, and knew I had recently been assigned to it as I was the best debugger in the whole world.

Anyway Jobs asked me if I like to suck cock. I said I didn't. He was disappointed and walked straight out of the room. That is why I was fired from Apple, not because of my mental illness.

But the day I left I was fortunate to be invited to dinner at the Mayor of Sacramento's house. He was a lovely fellow, and I was looking at his cultery and worked out a way to factor numbers in linear time. But when I was about to publish it, the FBI stepped in because all their encryption relied on the fact that large numbers can not be factorized easily. But I was whisked off the white house and briefed and told not to mention it to anyone.


\\\
  \ \        ^.^._______  This comment brought to you by the penis-nosed fox!
    \\______/_________|_)
    / /    \ \
    \_\_    \ \

OH GOD SPINLOCKING IN MALLOC! (none / 0) (#2)
by Pentashagon on Fri Jan 21, 2011 at 07:59:46 PM EST

NO WONDER THE IPHONE WAS A FAILURE!!!

After fighting with threads and synchronized()... (none / 0) (#3)
by claes on Fri Jan 21, 2011 at 09:30:59 PM EST

long enough, Erlang starts to make a lot of sense.

Anyhow, looks almost exactly like synchronized in Java.  It doesn't have to be self, right?  You
can synchronize on any object?

@synchronized() : under the hood | 4 comments (4 topical, 0 editorial, 0 hidden)
Display: Sort:

kuro5hin.org

[XML]
All trademarks and copyrights on this page are owned by their respective companies. The Rest © 2000 - Present Kuro5hin.org Inc.
See our legalese page for copyright policies. Please also read our Privacy Policy.
Kuro5hin.org is powered by Free Software, including Apache, Perl, and Linux, The Scoop Engine that runs this site is freely available, under the terms of the GPL.
Need some help? Email help@kuro5hin.org.
My heart's the long stairs.

Powered by Scoop create account | help/FAQ | mission | links | search | IRC | YOU choose the stories!