The original link: https://swift.gg/2018/07/30/friday-qa-2015-02-20-lets-build-synchronized/ author: Mike Ash, Sunnyyoung proofreading: wisdom multi-fiber finalized: Numbbbbb, CMB

The last article covered thread safety, and in this latest installment of Let’s Build, I’ll explore how to implement @synchronized in Objective-C. This article is based on the Swift implementation, and the Objective-C version is much the same.

review

@synchronized is a control structure in Objective-C. It takes an object pointer as an argument, followed by a block of code. The object pointer acts as a lock and is only allowed to be used by one thread in the @synchronized block at any one time.

This is an easy way to use locks for multithreaded programming. For example, you can use NSLock to protect operations on NSMutableArray:

NSMutableArray *array;
NSLock *arrayLock;

[arrayLock lock];
[array addObject: obj];
[arrayLock unlock];
Copy the code

You can also use @synchronized to lock the array itself:

@synchronized(array) {
    [array addObject: obj];
}
Copy the code

Personally, I prefer explicit locks to make things clearer, and @synchronized doesn’t perform as well, for reasons shown below. But it (@synchronized) is easy to use and fun to implement no matter what.

The principle of

The Swift version of @synchronized is a function. It takes an object and a closure and calls the closure with the held lock:

func synchronized(obj: AnyObject, f: Void -> Void){... }Copy the code

The question is, how do you turn any object into a lock?

In an ideal world (from the point of view of implementing this function), each object would leave some extra space for locking. Synchronized can use the appropriate lock and unlock methods in this extra small space. But there is no such extra space. This can be a good thing because it increases the memory footprint of the object, but most objects will never use this feature.

Another approach is to use a table to record the mapping of objects to locks. Synchronized finds a lock in a table and performs lock and unlock operations. The problem with this approach is that the table itself needs to be thread-safe, requiring either its own lock or some special lockless data structure. It’s much easier to set a separate lock for the form.

To prevent locks from accumulating and becoming permanent, tables need to track lock usage and destroy or reuse locks when they are no longer needed.

implementation

NSMapTable is ideal for implementing tables that map objects to locks. It can set the address of the original object to a key, and it can save weak references to keys and values, allowing the system to automatically reclaim unused locks.

let locksTable = NSMapTable.weakToWeakObjectsMapTable()
Copy the code

The object stored in the table is an NSRecursiveLock instance. Because it is a class, it can be used directly in NSMapTable, which pthread_mutex_t cannot. @synchronized supports recursive semantics, as does our implementation.

The table itself also needs a lock. Spinlock is useful in this case because access to the table is short:

var locksTableLock = OS_SPINLOCK_INIT
Copy the code

With this table, we can implement the following methods:

func synchronized(obj: AnyObject, f: Void -> Void) {
Copy the code

The first thing it does is find the lock corresponding to obj in the locksTable. The locksTableLock lock must be held before performing this operation:

OSSpinLockLock(&locksTableLock)
var lock = locksTable.objectForKey(obj) as! NSRecursiveLock?
Copy the code

If no lock is found in the table, create a new lock and save it to the table:

if lock == nil {
    lock = NSRecursiveLock() locksTable.setObject(lock! , forKey: obj) }Copy the code

Once the lock is in place, the primary table lock can be released. To avoid deadlocks this must be done before calling f:

OSSpinLockUnlock(&locksTableLock)
Copy the code

Now we can call f, locking and unlocking before and after the call:

lock! .lock() f() lock! .unlock() }Copy the code

Compare apple’s plan

Apple’s implementation of @synchronized can be found in the Objective-C Runtime source code:

http://www.opensource.apple.com/source/objc4/objc4-646/runtime/objc-sync.mm

Its main goal is performance, so it’s not as simple as the toy-like example above. It’s very interesting to see how they differ from each other.

The basic concept is the same. There is a global table that maps object Pointers to locks that are then unlocked before and after the @synchronized block.

For the underlying lock object, Apple uses pthread_mutex_t configured as a recursive lock. NSRecursiveLock probably uses pthread_mutex_t internally as well, eliminating the middleman and Foundation dependency at runtime.

The implementation of the table itself is a linked list rather than a hash table. It is common for only a few locks to exist at any given time, so linked lists perform well, perhaps better than hash tables. Each thread caches the locks that were recently looked up in the current thread, further improving performance.

Instead of just one global table, Apple’s implementation stores 16 tables in an array. Objects are mapped to different tables by address, which reduces unnecessary resource contention caused by @synchronized operations on different objects, since they are likely using two different global tables.

Apple’s implementation does not use weak pointer references (which can add a lot of overhead), but instead preserves an internal reference count for each lock. When the reference count reaches zero, the lock can be reused by new objects. Unused locks are not destroyed, but reuse means that the total number of locks at any one time cannot exceed the number of active locks, which means that the number of locks does not grow indefinitely with the creation of new objects.

Apple’s implementation is clever and performs well. But it still incurs some unavoidable extra overhead when compared to using a separate explicit lock. In particular:

  1. If unrelated objects happen to be assigned to the same global table, they can still compete for resources.
  2. Typically when looking for a nonexistent lock in the thread cache, a spin lock must be acquired and released.
  3. More work must be done to find locks on objects in the global table.
  4. Each lock/unlock cycle incurs recursive semantic overhead, even if it is not needed.

conclusion

@synchronized is an interesting language construct that is not easy to implement. Its purpose is to implement thread-safety, but its implementation itself requires synchronization operations to ensure thread-safety. While we use global locks to protect access to lock tables, Apple’s implementation uses a different trick to improve performance.