Like the comment and hope it gets better and better with your help

Author: @ios growth refers to north, this article was first published in the public number iOS growth refers to north, welcome to the correction

Please contact me if there is a need to reprint, remember to contact oh!!

preface

In ibireme’s no longer secure OSSpinLock article, there is a simple picture comparing the performance of various locks:

As you can see from the figure above, @synchronized is the worst performance of iOS multithreaded synchronization locks. But it is one of the easiest locks to use.

In general, we use it as in the following example:

@synchronized (self) {
  // code
}
Copy the code

So that the code in {} is thread-safe in the case of multiple threads? Notice, here we have one, right? Inappropriate use of @synchronized can also lead to thread safety issues.

@synchronizedThe principle of

When we want to explore the underlying implementation of a method, we can explore the implementation of this part of the code through the assembly section.

There are two ways to view the assembly section

  • Xcode–> Debug –>Debug Workflow –> Always Show DisassemblyDisplay assembly, then hang the breakpoint and run the program
  • Xcode–> Product–>Perform Action –> Assemble **.mfile

When we are in the test project, type the following code:

- (void)viewDidLoad {
    [super viewDidLoad];
    @synchronized (self) {
        NSLog(@"iOS 成长指北");
    }
}
Copy the code

Here, we use the second method to look at the assembly section, which makes it easy to find the specific location of the code. When we search for: line count, we find an assembly of specific code, as shown in the red box example.

When we call the NSLog method, there is one _objc_sync_enter and two _objc_sync_exit. As you can see, _objc_sync_exit is executed again when the code leaves the {} closure.

Xiao Yu, in his “More Than You Ever Wanted to Know about @Synchronized” [1], says that @synchronized blocks become a pair of calls to objc_sync_enter and objc_sync_exit. From the assembly call, it doesn’t seem like it?

After the release method is executed, objc_sync_exit is called once more.

The source code parsing

We can look for the above two methods and finally find _objc_sync_enter and _objc_sync_exit in

. Let’s look at the implementation

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

// 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(a); }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;
}


// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR

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(a);if(! okay) { result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR; }}}else {
        // @synchronized(nil) does nothing
    }
	

    return result;
}

Copy the code

From the source code and comments, we can see:

  • @synchronizedCreated a base onobjkeyThe recursive mutex lock ofrecursive_mutex_t mutex
  • whenobjnilWhen,_objc_sync_enter_objc_sync_exitNothing is done
  • What we finally locked to unlock isSyncDataThe structure is usedid2data(obj, usage)In order to get the
  • SyncDataThe essence should be a linked list header, as usednextDataFind and determine the corresponding value

objThe role of

Why do we need to pass an OBJ when we use @synchronized? Let’s look at the timing of using OBJ

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL; . }Copy the code

When we use it, we get SyncData corresponding to OBj and spinlock_t by StripedMap.

/* Fast cache: two fixed pthread keys store a single SyncCacheItem. This avoids malloc of the SyncCache for threads that only synchronize a single object at a time. SYNC_DATA_DIRECT_KEY == SyncCacheItem.data SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount */
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    SyncList() : data(nil), lock(fork_unsafe_lock) { }
};


// Use multiple parallel lists to decrease contention among unrelated objects.
// Using multiple parallel lists reduces contention between unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
Copy the code

StripedMap is essentially a hash table with an array in the outer layer. Each position in the array stores a linked-list-like structure, SyncList.

The reason for using hash tables is to avoid competing with multiple OBJs, whose hash functions are based on OBJs and not others. When we use id2data(obj, usage) to get the specified SyncData, we first get the corresponding SyncData from the hash(obj), so what do we do next?

Let’s look at other implementations of ID2Data (obj, Usage)

id2data(obj, usage)

If we want to know exactly how to get it, we need to look at it, right

static SyncData* id2data(id object, enum usage why)
{...#if SUPPORT_DIRECT_THREAD_KEYS
    // Check if there are matching objects in the 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);
            switch(why) {
            case ACQUIRE: {
              // Lock
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                // Unlock operation
                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; }}#endif

    // When we don't get SyncData from the thread cache we need to get SyncData from the thread cache that owns the lock
    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;
        }
        
    }
  
    lockp->lock();

    {
        ...
          // To create a cache, the current cache type is used to determine which thread cache is stored in
        goto done;
    }

    // malloc a new SyncData and add to list.
    // XXX calling malloc with a global lock held is bad practice,
    // might be worth releasing the lock, mallocing, and searching again.
    // But since we never free these guys we won't be stuck in malloc very often.
    
 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 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++; }}return result;
}
Copy the code
  • When we get the header of SyncData in SyncList, we need to look up the corresponding SyncData in the linked list.

  • When caching is present, the method implementation for finding the corresponding SyncData varies depending on whether SUPPORT_DIRECT_THREAD_KEYS is supported or not. One is based on TLS and the other is using the **for loop.

  • When there is no cache, we need to create the corresponding cache.

    • As we said earlier,SyncListThere is a spin lockspinlock_t lockWhen the thread cache can’t find anything, it adds a spin lock. butspinlock_t lockIt’s just a mutex named a spin lockos_unfair_lockJust.
    • It is worth noting that when multithreading, the corresponding threads may use the sameobj, but does not create a thread cache, i.eSyncDataDoes exist, but the thread cache does not. ifSyncDataIt doesn’t exist. We need to create oneSyncData. Finally createSyncDataThe thread cache and returns the correspondingSyncDataAnd add a recursive mutex.

Careful @ synchronized (obj)

Why did we say at the beginning that @synchronized is not thread-safe, and thread-safe issues occur when we use an object as obj that may become nil?

for (NSInteger i = 0; i < 10000; i ++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized (self.array) { self.array = [NSMutableArray array]; }}); }Copy the code

This example is from Resources [2], with a slight modification of the number of times created. Real machine debugging may require fewer debugging times, and emulators may support more.

This example crashes because the ARC setArray: method performs a release operation, which causes self.array to be nil in a thread, while @synchronized (nil) does not perform a lock unlock, which causes the thread to crash.

conclusion

Of all thread-safe solutions, @synchronized is the majority of users’ choice because of its cost of use, but performance issues have long been a problem for others.

Why is @synchronized the worst performer? Because the operations involved are extremely complex, in addition to the regular lock and unlock operations, hash table addressing, cache fetching/cache creation, etc., in the worst case, N different OBJ creating multiple different SyncData, The mutex OS_UNFAIR_lock named spin lock is called to implement the cache.

The resources

About @ synchronized, here more than you want to know: yulingtianxia.com/blog/2015/1…

IOS – @ synchronized a: www.jianshu.com/p/56f9cfd94…


If you have any questions, please comment directly, and feel free to express anything wrong with the article. If you wish, you can spread the word by sharing this article.

Thank you for reading this! 🚀