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@synchronized
What he did.And then in Xcode forobjc_sync_enter
andobjc_sync_exit
Set a sign breakpoint. As you can see,@synchronized
The 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:
- Obj is not null
SyncData* data
To removedata->mutex
To lock - If obj is empty, run the command
obj_sync_nil
In fact, nothing is processed by viewing the source code.
Then the core must be hereSyncData
A: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 pointid2data
Source 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 oneSyncData
Storage 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.