In iOS multi-threaded development, it is inevitable to encounter data synchronization problems, one of the solutions is to prevent two threads from operating on the same memory space by locking. Today we’ll focus on exploring a more common type of lock called @synchronized.

Code sample

Let’s start with a simple piece of code that can be converted to assembly code by Xcode@synchronizedWhat he did.And then in Xcode forobjc_sync_enterandobjc_sync_exitSet a sign breakpoint. As you can see,@synchronizedThe code block does call the above two functions, so let’s take a look at the source code.

Source code analysis

objc_sync_enter

// Begin synchronizing on 'obj'. 
// Allocates recursive 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);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}
Copy the code

A simple translation of the notes is as follows:

Start synchronization of obJ, open a mutex recursive lock associated with OBJ if necessary, and return OBJC_SYNC_SUCCESS when the lock is acquired.

From comments we can conclude that @synchronized is a mutex recursive lock.

The main logic of objc_sync_Enter is as follows:

  1. Obj is not nullSyncData* dataTo removedata->mutexTo lock
  2. If obj is empty, run the commandobj_sync_nilIn fact, nothing is processed by viewing the source code.

Then the core must be hereSyncDataA:This structure is very reminiscent of linked lists, where

Now let’s look at the method that gets data, ID2data

id2data

This method is more code, a total of more than 160 lines, you can open a copy of objC source synchronized viewing. The figure above is roughly divided into six operations, which we will analyze step by step.

Operation 1

spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
Copy the code

LOCK_FOR_OBJ and LIST_FOR_OBJ macros

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
Copy the code

Take a quick look at the StripedMap

template<typename T> class StripedMap { #if TARGET_OS_IPHONE && ! TARGET_OS_SIMULATOR enum { StripeCount = 8 }; #else enum { StripeCount = 64 }; Struct PaddedT {T value alignas(CacheLineSize); }; // array to store PaddedT PaddedT array[StripeCount]; Uintptr_t addr = reinterpret_cast<uintptr_t>(p); return ((addr >> 4) ^ (addr >> 9)) % StripeCount; } public: T& operator[] (const void *p) { return array[indexForPointer(p)].value; }...Copy the code

A StripedMap is actually a hash table with an array of capacity 8 that stores data of type T, or in this case, SyncList

struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
Copy the code

From this step, we can know that the main function of the above two macros is to get the SyncList where OBj is located through the hash algorithm, and then extract the corresponding array data and spinlock_t lock.

To summarize, the main work of operation 1 is to hash out the array of the SyncList corresponding to the OBj we need to lock and a spinlock_t lock

2 operation

#if SUPPORT_DIRECT_THREAD_KEYS // Check per-thread single-entry fast cache for matching object // Check individual thread fast cache bool fastCacheOccupied = NO; SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY); if (data) { fastCacheOccupied = YES; If (data->object == object) {// Found a match in fast cache. Uintptr_t lockCount; result = data; LockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY); if (result->threadCount <= 0 || lockCount <= 0) { _objc_fatal("id2data fastcache is buggy"); } switch(why) {case ACQUIRE: {// ACQUIRE type lockCount++; tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount); break; } case RELEASE: // RELEASE lockCount--; tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount); // 0 indicates that the current thread has not locked the object. If (lockCount == 0) {// remove from fast cache tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL); // atomic because may collide with concurrent ACQUIRE OSAtomicDecrement32Barrier(&result->threadCount); } break; case CHECK: // do nothing break; } return result; } } #endifCopy the code

If ACQUIRE/RELEASE is passed in, select the corresponding operation. If ACQUIRE/RELEASE is passed in, select the corresponding operation. Change lockCount and threadCount, and update TLS values.

Operation 3

If the corresponding SyncData is not found in TLS, operation 3 is entered.

Check per-thread cache of already-owned locks for matching objects SyncCache *cache = fetch_cache(NO); if (cache) { unsigned 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; if (result->threadCount <= 0 || item->lockCount <= 0) { _objc_fatal("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; } return result; }}Copy the code

Take a look at the implementation of SyncCacheItem and fetch_cache

typedef struct { SyncData *data; unsigned int lockCount; // number of times THIS THREAD locked this block } SyncCacheItem; static SyncCache *fetch_cache(bool create) { _objc_pthread_data *data; data = _objc_fetch_pthread_data(create); if (! data) return NULL; if (! data->syncCache) { if (! create) { return NULL; } else {// Default cache size is 4 int count = 4; data->syncCache = (SyncCache *) calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem)); data->syncCache->allocated = count; Allocated slot == // Make sure there's at least one open slot in the list. // If (data->syncCache-> Allocated slot == data->syncCache->used) { data->syncCache->allocated *= 2; data->syncCache = (SyncCache *) realloc(data->syncCache, sizeof(SyncCache) + data->syncCache->allocated * sizeof(SyncCacheItem)); } return data->syncCache; }Copy the code

Operation 3 fetches the corresponding SyncData from the SyncCache and then performs a similar operation to operation 2.

Operation of 4

The cache has not been created

/ / lock lockp - > lock (); { SyncData* p; SyncData* firstUnused = NULL; SyncData for (p = *listp; p ! = NULL; p = p->nextData) { if ( p->object == object ) { result = p; // The current thread is locked for the first time on the object. At this time need threadCount + 1 OSAtomicIncrement32Barrier (& result - > threadCount); goto done; } // If threadCount is 0, 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 = (objc_object *)object; result->threadCount = 1; goto done; }}Copy the code

The summary of this step is that if the cache has not yet been created, Syncdata needs to be looked for in the global listP list. If this is not found, but a null node is found, the null node is assigned.

Operation of 5

Posix_memalign ((void **)&result, alignOF (SyncData), sizeof(SyncData)); result->object = (objc_object *)object; result->threadCount = 1; new (&result->mutex) recursive_mutex_t(fork_unsafe_lock); Result ->nextData = *listp; *listp = result;Copy the code

If there are no null nodes in listp, create a new node and insert it into listp

Operating 6

Done: // The syncdata corresponding to the object has been created and stored, and there is no risk for multi-threading. if (result) { ... #if SUPPORT_DIRECT_THREAD_KEYS // If the cache is not already occupied, store the cache if (! fastCacheOccupied) { // Save in fast thread cache tls_set_direct(SYNC_DATA_DIRECT_KEY, result); tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1); } else #endif {// Save in thread cache // create cache if (! cache) cache = fetch_cache(YES); cache->list[cache->used].data = result; cache->list[cache->used].lockCount = 1; cache->used++; } } return result;Copy the code

Id2data summary

At this pointid2dataSource code finally analysis finished, summary, essence is a search object corresponding syncData process, first from TLS i.e. fast cache to find, then from the thread syncCache to find, and finally from the global listP list to find, if you can not find it can only create their own, and then store to the corresponding location.

Just to make it easier to understand, paste oneSyncDataStorage structure diagram ofAs you can see from the previous code, in ios, the global hash table size is 8.

objc_sync_exit

int objc_sync_exit(id obj) { int result = OBJC_SYNC_SUCCESS; if (obj) { SyncData* data = id2data(obj, RELEASE); if (! data) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; } else { bool okay = data->mutex.tryUnlock(); if (! okay) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; } } } else { // @synchronized(nil) does nothing } return result; }Copy the code

After analyzing ID2data, objc_sync_exit is relatively simple, which is to find the corresponding SyncData and unlock it without doing too much analysis.

@synchronized use caution points

First let’s look at the following code:A crash occurred during execution because the assignment code was executed by two threads at the same time, causing the old _testArray value to be released twice.The result is still crashed, the reason is also caused by excessive release, you may be wondering, why I add the lock and still not work?

The reason is mainly attributed to @synchronized (_testArray), because it locks _testArray. When _testArray changes, the objects locked in the subsequent thread and the previous thread have changed, that is, the syncData retrieved by different threads is different. Because the Object is no longer the same, the lock is invalidated, causing subsequent crashes. But if we lock self, we can solve this problem.