If you’ve written any concurrent programs in Objective-C, you’ve probably seen @synchronized. The @synchronized structure does something similar to a lock: it prevents different threads from executing the same piece of code at the same time. But in some cases, @synchronized is more convenient and readable than creating lock objects, adding and unlocking locks using NSLock.
Note: This is a little different from what the Apple documentation says about @synchronized, but pretty much the same. Apple’s official documentation emphasizes that it “prevents different threads from acquiring the same lock at the same time.” Because the documentation focuses on the various locks in multithreaded programming, it emphasizes “same lock” rather than “same code.”
If you haven’t used @synchronized before, here’s an example of how to use it. This article is essentially a brief study of how I implemented @synchronized.
Use the example of @synchronized
Suppose we were implementing a thread-safe queue in Objective-C, we might start by doing this:
123456789101112131415161718192021222324 @implementation ThreadSafeQueue{ NSMutableArray *_elements; NSLock *_lock; }- (instancetype)init{ self = [super init]; if (self) { _elements = [NSMutableArray array]; _lock = [[NSLock alloc] init]; } return self; }- (void)push:(id)element{ [_lock lock]; [_elements addObject:element]; [_lock unlock]; }@endCopy the code
The ThreadSafeQueue class above has an init method that initializes a _elements array and an NSLock instance. The class also has a push: method that acquires the lock, inserts elements into the array, and finally releases the lock. There may be many threads calling the push: method at the same time, but the line [_elements addObject: Element] will only be running on one thread at a time. The steps are as follows:
- Thread A calls
push:
methods - Thread B calls
push:
methods - Thread B calls
[_lock lock]
– Thread B acquires the lock because no other thread currently holds the lock - Thread A calls
[_lock lock]
But the lock is already occupied by thread B so the method call does not return – this suspends thread A’s execution - Thread B to
_elements
Called after the element is added[_lock unlock]
. When this happens, thread A’s[_lock lock]
Method and continues to insert its own element_elements
.
We can do this more briefly with the @synchronized construct:
12345678910111213141516171819202122 @implementation ThreadSafeQueue{ NSMutableArray *_elements; }- (instancetype)init{ self = [super init]; if (self) { _elements = [NSMutableArray array]; } return self; }- (void)increment{ @synchronized (self) { [_elements addObject:element]; }}@endCopy the code
In the previous example, “synchronized block” has the same effect as [_lock lock] and [_lock unlock]. You can think of it as locking self, as if self were an NSLock. The lock is acquired before any code after the open parenthesis {is run, and released before any code after the close parenthesis} is run. That’s good because mom doesn’t have to worry about me forgetting to call unlock anymore!
You can add @synchronized to any Objective-C object. We can substitute @synchronized(_elements) for @synchronized(self) in the above example, with the same effect.
Back to the research
I was curious about the @synchronized implementation and googled it for some details. I found some answers, but none of them went as far as I wanted. How does the lock relate to the object you pass in @synchronized? Does @synchronized retain (add reference count) locked objects? What happens if the @synchronized object you pass in is freed or assigned nil in the @synchronized block? These are all the questions I want to answer. And MY harvest this time, want you to look good 😏.
The @synchronized documentation tells us that the @synchronized block tacitly adds an exception handling to the protected code. So that if an exception is thrown when synchronizing an object, the lock is released.
The post on SO says that @synchronized block will become a pair of objc_sync_enter and objc_sync_exit calls. We don’t know what these functions do, but based on these facts we can assume that the compiler will write code like this:
123
@synchronized(obj) { // do work}
Copy the code
Into something like this:
123456
@try { objc_sync_enter(obj); // do work} @finally { objc_sync_exit(obj); }
Copy the code
What are objc_sync_Enter and objc_sync_exit? How are they implemented? Hold down the Command key in Xcode to click on them, and then go to
, which contains the two functions we are interested in:
1234567891011121314151617181920 /** * Begin synchronizing on 'obj'. * Allocates recursive pthread_mutex associated with 'obj' if needed. * * @param obj The object to begin synchronizing on. * * @return OBJC_SYNC_SUCCESS once lock is acquired. */OBJC_EXPORT int objc_sync_enter(id obj) __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0); /** * End synchronizing on 'obj'. * * @param obj The objet to end synchronizing on. * * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR */OBJC_EXPORT int objc_sync_exit(id obj) __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);Copy the code
A line at the bottom of the document reminds us: Apple engineers are human
1234567 // The wait/notify functions have never worked correctly and no longer exist.OBJC_EXPORT int objc_sync_wait(id obj, long long milliSecondsMaxWait) UNAVAILABLE_ATTRIBUTE; OBJC_EXPORT int objc_sync_notify(id obj) UNAVAILABLE_ATTRIBUTE; OBJC_EXPORT int objc_sync_notifyAll(id obj) UNAVAILABLE_ATTRIBUTE;Copy the code
The original source is older, so I replaced it with the latest header source.
However, objc_sync_enter’s documentation tells us something new: the @synchronized structure assigns a recursive lock to incoming objects at work. When and how does assignment happen? How does it deal with nil? Fortunately, Objective-C Runtime is open source, so we can read the source code right away and find out!
Note: Recursive locks do not generate deadlocks when acquired repeatedly by the same thread. You can find an elegant example of how it works here. There’s a ready-made class called NSRecursiveLock that does the same thing, and you can try it out.
You can find the full source code for ObjC-Sync here, but I’m going to take you through the source code and make you fly. Let’s start with the data structure at the top of the file. I’ll explain immediately below the code block, so don’t take too long to try to understand the code.
123456789101112131415161718 typedef struct SyncData { id object; recursive_mutex_t mutex; struct SyncData* nextData; int threadCount; } SyncData; typedef struct SyncList { SyncData *data; spinlock_t lock; } SyncList; // Use multiple parallel lists to decrease contention among unrelated objects.#define COUNT 16#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].datastatic SyncList sDataLists[COUNT];Copy the code
At the beginning, we have a definition of struct SyncData. This structure contains an object (well, the object we passed to @synchronized) and an associated recursive_mutex_t, which is the lock associated with object. Each SyncData also contains a pointer to another SyncData object called nextData, so you can think of each SyncData structure as an element in a linked list. Finally, each SyncData object contains a threadCount, which is the number of threads that use or wait for locks in the SyncData object. This is useful because SyncData structures are cached, and threadCount==0 implies that the SyncData instance can be reused.
Struct SyncList is defined below. As I mentioned above, you can think of SyncData as a node in a linked list. Each SyncList structure has a pointer to the head of the list of SyncData nodes and a lock that prevents multiple threads from making concurrent changes to the list.
The last line of the code block above is the sDataLists declaration – an array of 16 SyncList structures. Maps an incoming object to a subscript on an array using a defined hash algorithm. It is worth noting that this hash algorithm is cleverly designed to convert the memory address of the object pointer to an unsigned integer and shift it five places to the right, followed by a bitwise sum of 0xF, so that the result does not exceed the size of the array. The two macros, LOCK_FOR_OBJ(obj) and LIST_FOR_OBJ(obj), are better understood by first hashing the array subscript of the object and then fetching the lock or data of the corresponding element of the array. It all makes sense.
If you want to get free interview materials, you can get them in your skirt or add your friends. There is an iOS communication circle: **563513413** can come to understand, share the BAT, Ali interview questions, interview experience, discuss technology, skirt information directly download line, we exchange learning together!
When you call objc_sync_Enter (obj), it looks for the appropriate SyncData using the hash of obJ’s memory address and locks it. When you call objc_sync_exit(obj), it looks for the appropriate SyncData and unlocks it.
Translator’s note: The above source code and several paragraphs of explanation some original interpretation is not clear and omissions, I read the source code according to their own understanding of the supplement and correction.
Oh yeah! Now that we know how @synchronized associates a lock with the object you’re synchronizing, I want to talk about what happens when an object is released or set to nil in an @synchronized block.
If you look at the source code, you will notice that there is no retain and release in objc_sync_Enter. So it either does not hold the object passed to it, or it is compiled under ARC. We can test this with the following code:
12345678910 NSDate *test = [NSDate date]; // This should always be `1`NSLog(@"%@", @([test retainCount])); @synchronized (test) { // This will be `2` if `@synchronized` somehow // retains `test` NSLog(@"%@", @([test retainCount])); }Copy the code
Both times the output is 1. So objc_sync_enter doesn’t seem to hold the object passed in. Now this is interesting. If the object you are synchronizing is freed, then it is possible for another new object to be allocated memory here. It is possible that some other thread is trying to synchronize the new object (that is, the newly created object at the memory address of the freed old object). In this case, another thread will block until the current thread terminates its synchronization block. That doesn’t seem so bad. It sounds as if the people who do this already know and accept it. I haven’t come across any good alternatives.
If you want to get free interview materials, you can get them in your skirt or add your friends. There is an iOS communication circle: **563513413** can come to understand, share the BAT, Ali interview questions, interview experience, discuss technology, skirt information directly download line, we exchange learning together!
What if the object is set to nil in a synchronized block? Let’s review the implementation of our “naive” :
12345678910 NSString *test = @"test"; @try { // Allocates a lock for test and locks it objc_sync_enter(test); test = nil; } @finally { // Passed `nil`, so the lock allocated in `objc_sync_enter` // above is never unlocked or deallocated objc_sync_exit(test); }Copy the code
Objc_sync_enter is called test and objc_sync_exit is called nil. Objc_sync_exit is an empty operation when you pass nil, so no one will release the lock. This is fucked!
If Objective-C is susceptible to this, do we know? The following code calls @synchronized and sets a pointer to nil in the @synchronized block. The background thread then calls @synchronized on a pointer to the same object. If setting an object nil in the @synchronized block causes a deadlock, the code in the second @synchronized block will never execute. We will not see anything printed in the console.
123456789101112131415161718192021222324252627282930 NSNumber *number = @(1); NSNumber *thisPtrWillGoToNil = number; @synchronized (thisPtrWillGoToNil) { /** * Here we set the thing that we're synchronizing on to `nil`. If * implemented naively, the object would be passed to `objc_sync_enter` * and `nil` would be passed to `objc_sync_exit`, causing a lock to * never be released. */ thisPtrWillGoToNil = nil; }dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^ { NSCAssert(! [NSThread isMainThread], @"Must be run on background thread"); /** * If, as mentioned in the comment above, the synchronized lock is never * released, then we expect to wait forever below as we try to acquire * the lock associated with `number`. * * This doesn't happen, so we conclude that `@synchronized` must deal * with this correctly. */ @synchronized (number) { NSLog(@"This line does indeed get printed to stdout"); }});Copy the code
When we execute the above code, that line does print to the console! So Objective-C handles this situation pretty well. I bet the compiler does something like the following to solve this problem.
12345678 NSString *test = @"test"; id synchronizeTarget = (id)test; @try { objc_sync_enter(synchronizeTarget); test = nil; } @finally { objc_sync_exit(synchronizeTarget); }Copy the code
Implemented this way, the objects passed to objc_sync_Enter and objC_sync_exit are always the same. They’re both null operations when they pass in nil. This leads to a tricky debug scenario: if you pass nil to @synchronized, then you won’t get any locks and your code won’t be thread-safe! If you want to know why you’re receiving unexpected races, make sure you’re not passing nil to your @synchronized. You can do this by setting a symbolic breakpoint on objc_sync_nil. Objc_sync_nil is an empty method that will be called when the objc_sync_enter function is passed to nil to make debugging easier.
Objc_sync_nil objc_sync_nil objc_sync_nil objc_sync_nil objc_sync_nil
123456789101112131415161718192021 int objc_sync_enter(id obj){ int result = OBJC_SYNC_SUCCESS; if (obj) { SyncData* data = id2data(obj, ACQUIRE); require_action_string(data ! = NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed"); result = recursive_mutex_lock(&data->mutex); require_noerr_string(result, done, "mutex_lock failed"); } 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(); }done: return result; }Copy the code