preface

Locks in IOS are one of the most confusing issues. Locks are known to exist, but they are not often used. Today take you into the bottom of the lock world

The preparatory work

  • Objc – 818.2 –

The type of lock

There are basically three types of locks: spin locks, mutex locks and read/write locks

spinlocks

The thread repeatedly checks whether the lock variable is available, and is in a busy and equal state because it keeps executing during this process. Once a spin lock is acquired, the thread holds it until the display releases the spin lock. Spin-locks avoid the scheduling overhead of thread context and are therefore effective in situations where threads block only briefly

Advantages and disadvantages of spinlocks

  • Advantages: The spin lock does not cause the caller to sleep, avoids the scheduling overhead of the thread, and preferentially uses the spin lock if the lock is available for a short period of time
  • Cons: The spin lock is always occupiedcpuIn the case that the lock is not acquiredBusy etc.Greatly reducedcpuThe efficiency. It can also be seen that spin locks cannot be used recursively

Spin lock type

Common types of spin locks

  • OSSpinLock
  • atomic

The mutex

A mechanism in multithreaded programming that prevents multiple threads from reading or writing to the same common resource (such as a global variable). This is done by slicing code into critical sections. In fact, it is simple to say that one thread is performing the task at a time, and the other threads are asleep

Advantages and disadvantages of mutex

  • Advantages: When a locked resource is called, the caller’s thread sleeps.cpuOther threads can be scheduled to work. Therefore, mutex is recommended for complex tasks with long duration
  • Disadvantages: It is not a disadvantage personally, mutex is involved in thread scheduling overhead, if the task time is very short, thread scheduling can be reducedcpuThe efficiency of the

Mutex type

Common types of mutex

  • NSLock
  • pthread_mutex
  • @synchronized

Read-write lock

Read/write locks are suitable for situations where data structures are read more times than written. Since a read lock can be shared and a write lock can be exclusive, multiple write locks are called shared-exclusive locks

Lock performance data

There are many types of locks, but each lock has different performance. In the process of selecting the lock, try to select the lock with high performance

Simulator performance tests are as follows:

OSSpinLock -> os_UNfair_lock ->dispatch_semaphore_t -> pthread_mutex -> NSLock NSCondition -> pthread_mutex_recursive -> NSRecursiveLock -> NSConditionLock -> condition -> pthread_mutex_recursive -> NSRecursiveLock -> NSConditionLock -> conditionlock Synchronized

iPhoneXReal machine (Software version:13.7)

OSSpinLock, os_UNfair_lock, dispatch_semaphore_t, NSCondition, NSLock, NSLock, Pthread_mutex_t -> pthread_mutex_recursive -> NSRecursiveLock -> NSConditionLock -> conditionlock @synchronized

iPhoneXReal machine (Software version:13.7) performance figure

Summary: Some locks are not very different in performance, and the data may change slightly with each run, but the overall performance does not change much. The lowest performance is @synchronized, which you use a lot because it’s so easy to use. Apple has greatly improved @synchronized performance since the iPhone 12. There is no iPhone 12, but there will be an update if there is

@synchronized

Here’s a look at @synchronized, a common lock that people use because it’s convenient. Let’s explore how the @synchronized underlayer is implemented. A quick way to explore the underlying code is to follow the assembly process, and you can look at the underlying compiled code through Clang

Debugging code

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        @synchronized (appDelegateClassName) {
       
        }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code

The first kind of assembly debugging

The second way is to look at the underlying code compilation through Clang

The core methods through the above two approaches are the objc_sync_Enter and objc_Sync_exit methods, which are explored below. Before exploring, first find out which source library objc_sync_Enter and objc_sync_exit methods belong to, and give objc_Sync_Enter and objc_Sync_exit symbolic breakpoints

It clearly belongs to the Objc source library, as can be guessed from the name of the method

objc_sync_enterTo explore the

Search globally for objc_sync_enter in the Objc source library

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    // Check whether obj exists
    if (obj) {
        // Get the underlying encapsulation of SyncData
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock(a);/ / 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(a); }return result;
}
Copy the code
  • In the first place to judgeobjWhether it isnil, pay attention toobjisidType,idIs an object pointer typeobjc_object*
  • ifobjIt is worth going through the lock process
  • ifobj = nilAccording to the comments@synchronized(nil) does nothingNothing. Call it from insideobjc_sync_nil()methods

Globally search the objc_sync_nil() method

#   define BREAKPOINT_FUNCTION(prototype)                             \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }
    
    
 BREAKPOINT_FUNCTION(
    void objc_sync_nil(void));Copy the code

BREAKPOINT_FUNCTION is a macro definition void objc_sync_nil(void) is an argument. Prototype = define BREAKPOINT_FUNCTION(prototype); the prototype implementation does nothing. If obj = nil, it is unlocked

Conclusion: The objc_sync_enter method is locked. If obj is not nil, it is locked. Otherwise, it is not locked

objc_sync_exitTo explore the

Search objC_sync_exit globally in the Objc source library

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    // Check whether obj exists
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if(! data) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; }else {
            bool okay = data->mutex.tryUnlock(a);/ / unlock
            if(! okay) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; }}}else {
        // if obj = nil do nothing
        // @synchronized(nil) does nothing
    }
    

    return result;
}
Copy the code

The objc_sync_exit and objc_sync_Enter methods correspond. The objc_sync_exit method is the unlock method, and if obj= nil does nothing, okay

Conclusion:

  • objc_sync_enterThe lock () method is used at the start of a task, whileobjc_sync_exitThe unlock () method is used to unlock a task at the end of the task. If the parameter isnilIt’s equivalent to unlocking without a lock. Here it is@synchronizedInternal implementation of the lock unlock function
  • inobjc_sync_enterMethods andobjc_sync_exitMethod hasid2dataMethod and lock unlock function is also passedid2dataMethod called by the return value of the. So let’s exploreid2datamethods

id2dataMethods to explore

Overall process sorting and data structure analysis

Id2data method source more, first look at the overall process, and then in each part of the detailed exploration

The overall flow in the figure is as follows

  • First of all, from thetls(thread-local storage)SyncDataIf it is found, go through the corresponding process
  • iftlsIf not, go to the thread cache to look for it, if there is a cached process in the cache
  • If the cache does not determine whether the hash table stores the correspondingSyncDataIf theSyncDataThe process of multithreading the same object as long as it exists
  • If none is created, it is the first time to enter the databaseSyncData

SyncData, StripedMap and SyncCache are mentioned in the source code

  • SyncDataStructure analysis
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;// Same data type unidirectional linked list form
    DisguisedPtr<objc_object> object;// Encapsulate object
    // How many threads lock the same object
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;/ / recursive locking
} SyncData;
Copy the code

SyncData is a structure type with four variables

  • Struct SyncData* nextData: form of a one-way linked list of the same data type as SyncData

  • DisguisedPtr< objC_object > Object: The object is encapsulated in the low-level to facilitate calculation comparison. Associated objects also have their encapsulation

  • ThreadCount: How many threads lock the same object

  • Recursive_mutex_t mutex: Recursive lock

  • StripedMap

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
// SyncList
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList(a) : data(nil), lock(fork_unsafe_lock) {}};// StripedMap
template<typename T>
class StripedMap {
#ifTARGET_OS_IPHONE && ! TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif.// Omit some code
}
Copy the code

StripedMap is a hash table with 8 SyncList stores in the real machine and 64 stores in other environments. SyncList is a structure with two variables SyncData *data and Lock

  • SyncCache
typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;// Set the number of SyncCacheItem memory Spaces
    unsigned int used;// The number of uses
    SyncCacheItem list[0];
} SyncCache;
Copy the code

SyncCache is a structure, one for each thread cache.SyncCacheItem represents information about each object lock. SyncCacheItem is also a structure that contains SyncData and lockCount the number of times the current thread locks the current object

tlsLook fordata

TLS Thread-local storage: A separate private space provided by the operating system for threads, usually with limited capacity. Each thread will have its own TLS

// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
//1. Find data from the thread's local store
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
    fastCacheOccupied = YES;// If fastCacheOccupied = YES
    // If data->object is equal to object
    if (data->object == object) {
        // Found a match in fast cache.
        uintptr_t lockCount;// The number of locks
        result = data;// Assign to result the data found in TLS
        // Initialize lockCount = 1 to store key = SYNC_COUNT_DIRECT_KEY in TLS
        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: {// The lock identifier
            lockCount++;//lockCount ++ indicates that the lock is recursive
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);// Update the lockCount value in TLS
            break;
        }
        case RELEASE:// Unlock identifier
            lockCount--;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);// Update the lockCount value in TLS
            if (lockCount == 0) {
                // remove from fast cache
                // If lockCount = 0 means all current threads are unlocked, data in TLS is set to nil
                tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                // atomic because may collide with concurrent ACQUIRE
                // The number of threads threadCount is reduced by 1
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        case CHECK:
            // do nothing
            break;
        }
        // Return result directly
        returnresult; }}Copy the code

TLS Lookup process

  • First of all intlsTo check thedataIf thedataHave a valuefastCacheOccupied = YES
  • ifdata->object= =objectIt means that the same object is lockedtlsLook for thedataAssigned toresult
  • ifwhyisACQUIRELock, at this timelockCount++And put thelockCountUpdate to thetlsIn the
  • ifwhyisRELEASEIndicates unlocklockCount--And put thelockCountUpdate to thetls,If lockCount= =0Objects that are not locked or have been unlocked in the current thread. At this timethreadCountReduction of1To return toresult
  • ifdata->objectandobjectThe thread cache lookup process is performed if the object is not the same

Thread cache lookup flow

If the fetch_cache(NO) parameter is NO, it is only to look up
SyncCache *cache = fetch_cache(NO);
if (cache) {
    unsigned int i;
    // Iterate over SyncCacheItem in the cache
    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 an item is found in the thread cache, assign item->data to result
        if (result->threadCount <= 0  ||  item->lockCount <= 0) {
            _objc_fatal("id2data cache is buggy");
        }
            
        switch(why) {
        case ACQUIRE:// The lock identifier
            item->lockCount++;
            break;
        case RELEASE:// Unlock identifier
            item->lockCount--;
            if (item->lockCount == 0) {
                // remove from per-thread cache
                // Set the data in the current item to empty data, and then cache->used to reduce the number of used data
                cache->list[i] = cache->list[--cache->used];
                // atomic because may collide with concurrent ACQUIRE
                // The number of threads threadCount is reduced by one
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        case CHECK:
            // do nothing
            break;
        }
        // Return result if it is found in thread cache
        returnresult; }}Copy the code

The thread cache lookup process is basically similar to the TLS lookup process, except that the data structure type is different. Loop through the thread cache to find objects that need to be locked. The detailed process is noted above

  • fetch_cacheTo explore the
static SyncCache *fetch_cache(bool create)
{
    _objc_pthread_data *data;
    
    // Query or create thread data
    data = _objc_fetch_pthread_data(create);
    if(! data)return NULL;// Return NULL if data = nil

    if(! data->syncCache) {if(! create) {return NULL;
        } else {
            // Create four SynccacheItems for the first time
            int count = 4;
            // Open up memory space
            data->syncCache = (SyncCache *)
                calloc(1.sizeof(SyncCache) + count*sizeof(SyncCacheItem));
            // The number of initializations is equal to 4data->syncCache->allocated = count; }}// Make sure there's at least one open slot in the list.
    If the initial number is the same as the number in use, expand the number twice
    if (data->syncCache->allocated == data->syncCache->used) {
        // Double the capacity
        data->syncCache->allocated *= 2;
        // Create space
        data->syncCache = (SyncCache *)
            realloc(data->syncCache, sizeof(SyncCache) 
                    + data->syncCache->allocated * sizeof(SyncCacheItem));
    }

    return data->syncCache;
}

Copy the code
  • According to thecreateTo find thread data_objc_pthread_dataIf thedataThere is no direct returnNULL
  • ifdata->syncCacheThere is no andcreate= =YESTo go toSyncCacheandSyncCacheItemOpen up memory space for the first time4aSyncCacheItem, the number of initializationsallocated = 4. eachSyncCacheItemThe default data in there is0
  • If the number of initializations is the same as the number of uses2Times the capacity. returnSyncCache

_objc_fetch_pthread_data inquiry

 _objc_pthread_data *_objc_fetch_pthread_data(bool create)
{
    _objc_pthread_data *data;
    // Go to TLS to find thread data, if data exists directly return
    // If data does not exist and create = NO returns nil
    data = (_objc_pthread_data *)tls_get(_objc_pthread_key);
    // If data does not exist and create = YES, create memory to create _objc_pthread_data
    if(! data && create) { data = (_objc_pthread_data *)calloc(1.sizeof(_objc_pthread_data));
        tls_set(_objc_pthread_key, data);
    }

    return data;
}
Copy the code

_objc_fetch_pthread_data process

  • intlsLook for_objc_pthread_dataIf the query result is displayed, the command output is displayeddata
  • ifdataThere is no andcreate = NOTo return tonil
  • ifdataThere is no andcreate = YES, to open up memory creation_objc_pthread_data

So create = NO will only look for data in TLS, create = YES will look for data, if not open memory to create data

Multiple threads lock the same object

The following code is a multi-thread lock consent object, because if it is a single thread memory operation will directly follow the ABOVE TLS lookup process and thread cache lookup process

lockp->lock(a);// Multithreaded operation process
{
    SyncData* p;
    SyncData* firstUnused = NULL;
    *listp has a value if SyncList data in the hash table has a value
    // Iterate over SyncData in the looping unidirectional list
    for(p = *listp; p ! =NULL; p = p->nextData) {
        // Query the object to be locked
        if ( p->object == object ) {
            result = p;// Assign
            // atomic because may collide with concurrent RELEASE
            ThreadCount adds 1 to threadCount. Multiple threads lock the object
            OSAtomicIncrement32Barrier(&result->threadCount);
            // Jump to the done process. The result is stored in the TLS or thread cache of each thread
            goto done;
        }
        if ( (firstUnused == NULL) && (p->threadCount == 0) ) firstUnused = p; }... }Copy the code

Multithreaded operation flow

  • First of all, based onobjectFind the subscript in the hash tableSyncListAnd then judgeSyncListthedataHave a value
  • ifSyncListthedataThere is a value in the value, then find the corresponding lock object in the one-way linked list, proceedthreadCount++Operation, then jumpdoneThe process,doneStored in the process in the corresponding threadtlsOr in the thread cache

createSyncData

posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
// Assign
result->object = (objc_object *)object;
result->threadCount = 1;// Default threadCount = 1
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);// Create a recursive lock
result->nextData = *listp; // Hash subscripts form linked lists in the same way as head interpolation
*listp = result;// Update the values in the hash table
Copy the code
  • The first time an object is locked, one is createdSyncDataIn short, it is oneSyncDataBind an object, and an object has one and only oneSyncData
  • If you hash different objects with the same index you’re going to have a one-way list, and you’re going to insert them by head

doneprocess

done:
    lockp->unlock(a);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
        // fastCacheOccupied = NO: There is NO data in TLS for the current thread
        // SyncData and lockCout = 1 are stored in TLS
        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
        {   // When another object is locked in the same thread, note that the last object is not unlocked.
            // SyncData and lockCount are stored in the thread cache
            // Save in thread 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

The done process

  • The first object in the current thread to perform the lock operation, and theSyncDataandlockCout=1Stored in thetls, and the current threadtlsWill not change unless unlocked. one-threadtlsOnly the first time the lock is storedSyncDataandlockCout
  • When another object is locked in the same thread (the last locked object was not unlocked)SyncDataandlockCountStored in the thread cache

So far the entire ID2data method of the source has been explored, the following case analysis

Case analysis

Single thread single object recursive locking

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWPerson * p = [LWPerson alloc];
        @synchronized (p) {
            NSLog(@"P comes in for the first time.");
            @synchronized (p) {
                NSLog(@"P came in the second time.");
                @synchronized (p ) {
                    NSLog(@"P comes in the third time.");
                };
            };
        };
 
    return 0;
}
Copy the code

Debug at the first @synchronized breakpoint

SQL > select * from listp, data, and cache; SQL > select * from TLS and thread cache; This will enter the SyncData creation process. For testing purposes, I set StripeCount = 2 in the non-real state

The graph shows that the data in the hash table is empty. There is no data. The breakpoint goes down one step and continues debugging

*listp = result nextData = *listp; *listp = result nextData = *listp; *listp = result *listp has been assigned a value. Continue debugging to the done process

The breakpoint in the figure shows that SyncData and lockCount, created for the first time at this time, are stored in TLS

Debug at the second @synchronized breakpoint

Select * from listp where data = 1 and lock = 2; select * from listp where data = 1 and lock = 2

At the third @synchronized breakpoint to debug

It is obvious to recursively lock the same object in a single thread, SyncData and lockCount are stored in TLS, and there is no caching

Single-thread multi-object recursive locking

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    LWPerson * p  = [LWPerson alloc];
    LWPerson * p1 = [LWPerson alloc];
    LWPerson * p2 = [LWPerson alloc];

    dispatch_async(dispatch_get_global_queue(0.0), ^{
        @synchronized (p) {
            NSLog(@"P comes in for the first time.");
            @synchronized (p1) {
                NSLog(@"P1 comes in the second time");
                @synchronized (p2) {
                    NSLog(@"P2 comes in the third time.");
                };
            };
        };
    });
    do{}while (1);
  }
    return 0;
}
Copy the code

The p object is locked after the first @synchronized breakpoint is set, and the SyncData and lockCount created are stored in TLS, as explored above

Debug at the second @synchronized breakpoint

* if the value of listp is set to the same index as p1, there is no data in the cache. If the value of listp is set to the same index as P1, there is no data in cache

The newly created SyncData is placed in the front of the zipper, and the nextData of P1’s SyncData stores P’s SyncData address

The cached cached version of fastCacheOccupied is occupied by the thread; the cached version of fastCacheOccupied is occupied by the thread; the cached version of fastCacheOccupied is occupied by the thread; Data in the cache will be cleared when unlocking

Multithreaded recursive locking

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWPerson * p  = [LWPerson alloc];
        LWPerson * p1 = [LWPerson alloc];
        dispatch_queue_t  queue = dispatch_queue_create("lw", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i<10; i++) {dispatch_async(queue, ^{
                @synchronized (p) {
                    NSLog(@"P comes in for the first time.");
                    @synchronized (p1) {
                        NSLog(@"P comes in for the first time.");
                    };
                };
            });
        }
        do{}while (1);
         
    }
    return 0;
}
Copy the code

Set a breakpoint at @synchronized (p1) for debugging

Only one SyncList in the hash table has a value, and the value threadCount = 10 in SyncData indicates that multithreading is going on. The value of nextData is empty, indicating that only one object is locked by multiple threads at this time. If multiple threads are applied to multiple objects, each object actually goes through the locking process of a single object

One way linked list diagram after fill

@synchronizedconclusion

  • objc_sync_exitThe process andobjc_sync_enterThe process is the same except one is to lock and one is to unlock
  • @synchronizedThe bottom layer is linked list lookup, cache lookup, and recursion, which is very memory – and performance-intensive
  • @synchronizedThe underlying package is a recursive lock that can be unlocked automatically, which is why people like to use it
  • @synchronizedIn thelockCountControl recursion, andthreadCountControl multithreading
  • @synchronizedLock objects as far as possible not tonilOtherwise, it won’t work

conclusion

The world of locks has just begun, and today explores @synchronized recursive locks. The following will explore other types of locks, let’s travel in the world of locks