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:

  1. IOS underlying principles of alloc exploration
  2. The underlying principles of iOS are explored
  3. The underlying principles of iOS explore the nature of objects & isa’s underlying implementation
  4. Isa-basic Principles of iOS (Part 1)
  5. Isa-basic Principles of iOS (Middle)
  6. Isa-class Basic Principles of iOS Exploration (2)
  7. IOS fundamentals explore the nature of Runtime Runtime & methods
  8. Objc_msgSend: Exploring the underlying principles of iOS
  9. Slow lookups in iOS Runtime
  10. A dynamic approach to iOS fundamentals
  11. The underlying principles of iOS explore the message forwarding process
  12. Dyld (part 1)
  13. IOS Basic Principles of application loading principle dyld (ii)
  14. IOS basic principles explore the loading of classes
  15. The underlying principles of iOS explore the loading of categories
  16. IOS underlying principles to explore the associated object
  17. IOS underlying principle of the wizard KVC exploration
  18. Exploring the underlying principles of iOS: KVO Principles | More challenges in August
  19. Exploring the underlying principles of iOS: Rewritten KVO | More challenges in August
  20. The underlying principles of iOS: Multi-threading | More challenges in August
  21. GCD functions and queues in iOS
  22. GCD principles of iOS (Part 1)
  23. IOS Low-level – What do you know about deadlocks?
  24. IOS Low-level – Singleton destruction is possible?
  25. IOS Low-level – Dispatch Source
  26. IOS bottom – a fence letter blocks the number
  27. IOS low-level – Be there or be Square semaphore
  28. IOS underlying GCD – In and out into a scheduling group
  29. 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 (staticStripedMap 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.