This is the 18th day of my participation in the August Genwen Challenge.More challenges in August
Write in front: iOS underlying principle exploration is my usual development and learning in the accumulation of a section of advanced road. Record my continuous exploration of the journey, I hope to be helpful to all readers.Copy the code
The directory is as follows:
- IOS underlying principles of alloc exploration
- The underlying principles of iOS are explored
- The underlying principles of iOS explore the nature of objects & isa’s underlying implementation
- Isa-basic Principles of iOS (Part 1)
- Isa-basic Principles of iOS (Middle)
- Isa-class Basic Principles of iOS Exploration (2)
- IOS fundamentals explore the nature of Runtime Runtime & methods
- Objc_msgSend: Exploring the underlying principles of iOS
- Slow lookups in iOS Runtime
- A dynamic approach to iOS fundamentals
- The underlying principles of iOS explore the message forwarding process
- Dyld (part 1)
- IOS Basic Principles of application loading principle dyld (ii)
- IOS basic principles explore the loading of classes
- The underlying principles of iOS explore the loading of categories
- IOS underlying principles to explore the associated object
- IOS underlying principle of the wizard KVC exploration
- Exploring the underlying principles of iOS: KVO Principles | More challenges in August
- Exploring the underlying principles of iOS: Rewritten KVO | More challenges in August
- The underlying principles of iOS: Multi-threading | More challenges in August
- GCD functions and queues in iOS
- GCD principles of iOS (Part 1)
- IOS Low-level – What do you know about deadlocks?
- IOS Low-level – Singleton destruction is possible?
- IOS Low-level – Dispatch Source
- IOS bottom – a fence letter blocks the number
- IOS low-level – Be there or be Square semaphore
- IOS underlying GCD – In and out into a scheduling group
- Basic principles of iOS – Basic use of locks
Summary of the above column
- Summary of iOS underlying principles of exploration
Sort out the details
- Summary of iOS development details
preface
In the last article, we looked at the basic use of locks. In the next article, we will explore the common locks used in iOS development. Today, we will take a look at the process of @synchronized mutex.
@synchronized Flow analysis
To prepare
Thread Loacl Storage (TLS)
The operating system provides a separate private space for threads, usually with limited capacity. On Linux, the pThread library is usually used
- pthread_key_create()
- pthread_getspecific()
- pthread_setspecific()
- pthread_key_delete()
Posix_memalign supports memory alignment
Compile to locate source code
To get an idea of the underlying structure, run xcrun in general and get the following in the.cpp file:
- Definition in the main function
@synchronized (appDelegateClassName) { }
Copy the code
- Compiled code:
{
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
/ / structure
struct _SYNC_EXIT {
// constructor
_SYNC_EXIT(id arg) : sync_exit(arg) {}
// destructor
~_SYNC_EXIT(){objc_sync_exit(sync_exit); } id sync_exit; } _sync_exit(_sync_obj); }catch(id e) {_rethrow = e; } { struct _FIN { _FIN(id reth) :rethrow(reth) {}
~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
id rethrow;
} _fin_force_rethow(_rethrow);}
}
Copy the code
After a brief analysis, we find the key points as follows:
try{}
The contents of the code block.objc_sync_enter(_sync_obj);
objc_sync_exit(sync_exit);
The global search did not find a specific implementation of the above content, next, we use the symbol breakpoint to see which library @synchronized is implemented in:
You can see that the implementation is in libobjc.a.dylib.
Below we through libobJC source code to analyze its internal implementation of the process.
The process to explore
objc_sync_enter
// start synchronization on 'obj'
// Assign the recursive mutex associated with 'obj' if necessary
// Once the lock is acquired, OBJC_SYNC_SUCCESS is returned
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
/ / obj
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
data->mutex.lock();
} else {
// obj doesn't exist and doesn't do anything
// @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();
}
returnresult; }... BREAKPOINT_FUNCTION(void objc_sync_nil(void)); .// Nothing is realized
BREAKPOINT_FUNCTION(void stop_on_error(void)); /* Use this method for functions intended as breakpoint hooks if they are not, the compiler may optimize them; * /
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }...Copy the code
objc_sync_exit
// end synchronization on 'obj'
// return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
/ / 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 {
//obj doesn't exist and doesn't do anything
// @synchronized(nil) does nothing
}
return result;
}
Copy the code
You can see that in objc_sync_Enter and objc_sync_exit nothing is done when the locked object obj is empty.
Data ->mutex.lock(); data->mutex.lock(); Data -> mutex.tryunlock (); Unlocking operations. One locks, one unlocks and that’s exactly what locks do.
Here we should also pay attention to the structure of data, namely SyncData, because it is the lock encapsulated in this data that does the locking and unlocking operation.
Let’s take a look at the data structure of SyncData and then focus on the internal implementation of id2Data to continue exploring the underlying implementation of @synchronized.
SyncData data structure
SyncData is a structure:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // The number of threads using this block
recursive_mutex_t mutex; / / recursive locking
} SyncData;
Copy the code
-
When I saw the first nextData pointer, I assumed that the SyncData was a one-way linked list structure (because the SyncData structure points to the next SyncData data through nextData).
-
ThreadCount records the number of threads used, which means multithreading is supported. So how exactly can you use @synchronized for multithreading? Let’s look at it through process analysis.
Id2data process analysis
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock){}};Use multiple parallel lists to reduce contention between unrelated objects
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// Global static variable hash table
static StripedMap<SyncList> sDataLists;
// The hash function determines a subscript
// Conflicts may occur (previously resolved by hashing)
// Use the zipper method here.static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
Thread-local storage is supported
// Check if the single-entry cache for each thread matches the object
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
// The first part
if (data) { ... }
#endif
// Check that each thread cache that already owns the lock matches the object
SyncCache *cache = fetch_cache(NO);
// Part 2
if (cache) { ... }
// The thread cache did not find anything
// Walk through the list in use to find matching objects
// A spin lock prevents multiple threads from creating multiple threads
// Lock the same new object
// We can store the nodes in a hash table, if any
// There are over 20 different locks in work, but we don't do that now.
// Here is a lock to ensure the security of the memory space, unlike the outside lock oh, here is a spinlock_t defined above
lockp->lock();
// Part 3
/ / code block{... }// Assign a new SyncData to the list.
// it is bad practice for XXX to allocate memory while holding a global lock,
// It might be worth releasing the lock, reassigning, and searching again.
// But since we never release these people we do not often get stuck in distribution.
// Part 4
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;
done:
lockp->unlock(); // Here is a lock to ensure the security of the memory space, unlike the outside lock oh, here is a spinlock_t defined above
// Part 5
if (result) { ... }
return result;
}
Copy the code
Part one and part two
The same thread is going to come in and process it.
If thread-local storage is supported, SyncData is returned through thread-local storage. If not, SyncData is accessed through the thread cache.
Get data (SyncData) from TLS, judge why, process ACQUIRE and RELEASE, and return data.
The first part
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: {
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
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;
}
returnresult; }}Copy the code
The process is similar to the first part, except that it is fetched from the cache and then operated on.
The second part
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;
}
returnresult; }}Copy the code
The third part
Different threads will process it here.
The third part does nothing when the object we lock first comes into the ID2Data process.
{
SyncData* p;
SyncData* firstUnused = NULL;
for(p = *listp; p ! = NULL; p = p->nextData) {if ( p->object == object ) {
result = p;
// Atomic, because it might conflict with concurrent releases
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// There is no SyncData currently associated with the object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// If you find one that hasn't been used, use it
if( firstUnused ! = NULL ) { result = firstUnused; result->object = (objc_object *)object; result->threadCount =1; goto done; }}Copy the code
The fourth part
Result is the SyncData currently retrieved from TLS or cache via the locked object. Pointing its nextData to itself; There is a head plug involved. That is, each time the new data is inserted in front of the previous data.
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
The fifth part
The previously queried data is stored in TLS or thread cache
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if(why ! = ACQUIRE) _objc_fatal("id2data is buggy");
if(result->object ! = object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
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
if(! cache) cache = fetch_cache(YES); cache->list[cache->used].data = result; cache->list[cache->used].lockCount =1; cache->used++; }}Copy the code
LLDB debugging analysis
Let’s use a custom example using @synchronized to debug:
SMPerson *p1 = [SMPerson alloc];
SMPerson *p2 = [SMPerson alloc];
SMPerson *p3 = [SMPerson alloc];
SMPerson *p4 = [SMPerson alloc];
SMPerson *p5 = [SMPerson alloc];
SMPerson *p6 = [SMPerson alloc];
dispatch_async(dispatch_queue_create("superman", DISPATCH_QUEUE_CONCURRENT), ^{
NSLog(@"0");
@synchronized (p1) {
NSLog(@"1");
@synchronized (p2) {
NSLog(@"2");
@synchronized (p1) {
NSLog(@"1-1");
@synchronized (p1) {
NSLog(@"1-1-1");
@synchronized (p3) {
NSLog(@"3");
@synchronized (p4) {
NSLog(@"4");
@synchronized (p5) {
NSLog(@"5");
@synchronized (p6) {
NSLog(@"6"); }}}}}}}}});Copy the code
Obj = p1 when you first come in; Null in TLS and cache.
You’ll come to part 4 (initialize a new SyncData and add it to the list) and Part 5 (store TLS and cache) where you actually do TLS storage.
Obj = p2 on the second entry;
After retrieving the last SyncData lock in TLS or cache,
The process is the same as above, but in Part 5 the thread cache is used instead of TLS storage
Obj = p1;
I’m going to go to the first part of the execution, and I’m going to go down and go into the ACQUIRE branch, lockCount++; The lock count is stored in TLS via SYNC_COUNT_DIRECT_KEY and SyncData retrieved from TLS storage is returned. The next time the same object is checked, SYNC_COUNT_DIRECT_KEY is also retrieved for the number of locks.
Obj = p1;
The order of execution is the same as last time.
conclusion
- The @synchronized lock, when used, maintains a global hash table (
static
StripedMap sDataLists;) , using the zipper method to store SyncData; - SDataLists, where arrays store SyncList (bind to objc, the object we lock);
- Objc_sync_enter and objc_Sync_exit call symmetrically and encapsulate a recursive lock;
- Two storage methods: TLS and thread caching;
- The first SyncData header was used in a linked list with threadCount = 1;
- Check if the object is the same: if so, lockCount++; If no, repeat the previous step.
- LockCount –, lockCount == 0: threadCount–;
Summarize the process with a diagram:
Why does @synchronized have reentrant, recursion and multithreading?
TLS stores the number of threadCount tokens; LockCount marks the number of locks; The locking of a lock object.
About SyncList structures
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock){}};Use multiple parallel lists to reduce contention between unrelated objects
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
// Global static variable hash table
static StripedMap<SyncList> sDataLists;
// The hash function determines a subscript
// Conflicts may occur (previously resolved by hashing)
// Use the zipper method here
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // The number of threads using this block
recursive_mutex_t mutex; / / recursive locking
} SyncData;
Copy the code
In the ID2Data process analysis, the sDataLists’ underlying structure, which varies slightly between architectural environments, stores SyncList, which stores SyncData, which is a linked list structure. Thus, the following zipper structure is formed:
TLS and Cache
In the code in Part 5:
{
// Save in thread cache
if(! cache) cache = fetch_cache(YES); cache->list[cache->used].data = result; cache->list[cache->used].lockCount =1;
cache->used++;
}
Copy the code
Details of this section: If the same thread is running the @synchronized lock, TLS cannot continue to be processed indefinitely (which puts a lot of strain on TLS), so TLS can only be accessed when the thread is switched. Other cases are stored in thread cache.