The original addressRykap.com/objective-c…


If you’ve ever worked with concurrent programming in Objective-C, you’ve probably seen the @synchronized structure. Synchronized is often used to avoid concurrent execution of the same piece of code. Using @synchronized makes code more readable and easier to understand than allocating and locking an NSLock object.

If you’ve never used @synchronized, here’s a simple example of how to use it. This article will provide a brief introduction to the @synchronized implementation.

@synchronized Usage examples

Assuming we implement a thread-safe queue using Objective-C, we might:

@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];
}

@end
Copy the code

ThreadSafeQueue has an init method that initializes a _elements array and an NSLock. In addition, there is a push: method that performs the lock, adding objects to and unlocking the array. Multiple threads call the push: method at the same time, but adding an object to an array can only be done by one thread at a time. In words, it is

  1. Thread A callspush:
  2. Thread B callspush:
  3. Thread B calls[_lock lock]– Thread B needs to lock since no other object holds the lock
  4. Thread A calls[_lock lock]But since thread B is holding the lock, this method does not return, but suspends thread A’s execution
  5. Thread B adds the object to the array and calls[_lock unlock]. When this operation completes, thread A’s[_lock lock]Method returns. Thread A starts to continue.

Using @synchronized, the code would be:

@implementation ThreadSafeQueue {
	NSMutableArray *_elements;
}

- (instancetype)init {
	self = [super init];
	if (self) {
		_elements = [NSMutableArray array];
	}
	return self;
}

- (void)increment {
	@synchronized (self) { [_elements addObject:element]; }}@end
Copy the code

Sychronized functions are the same as [_lock lock] and [_lock unlock] in the previous example. You can think of this code as using NSLock to lock self. Lock must be added after {and unlocked before}. This is relatively easy, since we no longer need to call the UNLOCK method!

You can apply @synchronized to other Objective-C objects, so we can replace @synchronized(self) with @synchronized(_elements), and get exactly the same result.

Go back to study

Curious about the implementation of @synchronized, I started googling about it. I found some answers, but they didn’t go as far as I was looking for. How does lock relate to objects passed into @synchronized? Does @synchronized retain the object during locking? That’s what I want to know, so I’m going to show you what I can find.

The @synchronized documentation tells us that the @synchronized block implicitly adds execution fragments to protected code. So if an exception is thrown when synchronizing a particular object, the lock object will also be released.

SO @synchronized is executed in objc_sync_enter and objc_sync_exit. We don’t know what these two lines of code do, but for a compiler it might look like this:

@synchronized(obj) {
	// do work
}
Copy the code

Will convert

@try {
	objc_sync_enter(obj);
	// do work
} @finally {
	objc_sync_exit(obj);
}
Copy the code

What are the functions and implementations of objc_sync_Enter and objc_Sync_Exit? In Xcode, CMD +click will go to ‘

‘ and we can see:

// Begin synchronizing on 'obj'
// Allocates recursive pthread_mutex associated with 'obj' if needed
int objc_sync_enter(id obj)

// End synchronizing on 'obj'
int objc_sync_exit(id obj)

// The wait/notify functions have never worked correctly and no longer exist.
int objc_sync_wait(id obj, long long milliSecondMaxWait);
int objc_sync_notify(id obj);
Copy the code

Ok, objc_sync_Enter’s documentation tells us something: @synchronized only works when it assigns a recursive lock to an object and passes it on. But when did the allocation take place? How is it distributed? How do I deal with nil values? Fortunately, the OC Runtime is open source and we can get the answer from it

You can see the full code for objC-Sync here, but I’ll take you into an easier understanding. Looking at the data structure at the top of the file, I will explain the data through the following examples, without spending too much time interpreting them.

typedef struct SyncData {
	id object;
	recursive_mutext_t mutex;
	struct SyncData* nextData;
	int threadCount;
} SyncData;

typedef struct SyncList {
	SyncData *data;
	spinlock_t lock;
}	SyncList;

static SyncList sDataLists[16];
Copy the code

First, we look at the definition of struct SyncData. This structure contains an object(which is the object we passed in) and an associated RECURsive_mutex_t (the lock associated with the object). Each SyncData contains a pointer to another SyncData called nextData, so you can treat each SyncData structure as an element in a linked list. Finally, each SyncData contains a threadCount object to indicate the number of threads currently in use or waiting for the SyncData object. So, if SyncData is cached, threadCount == 0 indicates that the SyncData instance is being reused.

Then we see the definition of struct SyncList. As mentioned above, SyncData can be treated as a node in a list. Each SyncList structure has a pointer to the starting address of SyncData, and lock prevents concurrent modification in multiple threads.

The last line is the sDataLists declaration for the array of SyncList structures. It may not seem like much at first glance, but the sDataLists array is a hash table that maps OC objects to their corresponding locks.

When you call objc_sync_Enter (obj), it uses the hash of obj’s memory address to find the associated SyncData and locks it. When you call objc_sync_exit, you’re looking for SyncData to unlock.

Great! Now we know how @synchronized works. The next thing I’m going to tell you about @synchronized is what happens when an object gets destroyed or becomes nil

If you read the source code, you’ll see that there’s no retains or releases in objc_Sync_Enter. So it doesn’t retain the object we passed in, or compile it through ARC. We can test the following with the following code:

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

This code prints two 1’s. So it looks like objc_sync_Enter will not retain the object passed to it. If the object is destroyed during synchronization, then a new object is allocated at this location. Other threads might treat the new object as the old one. In this example, other threads will block until the current thread completes. It doesn’t sound like much.

What if the object becomes nil?

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 will call test successfully, and objc_sync_exit will call nil. So in the end objc_sync_exit did not execute successfully, which means the lock was not released

The following code passes a pointer to @synchronized and becomes nil during execution. Then call @synchronized in the background thread. If in the first @synchronized object the object becomes nil before unlocking, the second @synchronized object is never executed. We don’t see any output.

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_DEFAULT_PRIORIT_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

However, when we run the code above, we can see the output in the console! So I guess OC can handle this situation by probably executing the following code:

NSString *test = @"test";
id synchronizedTarget = (id)test;
@try {
	objc_sync_enter(syncrhonizedTarget);
	test = nil;
} @finally {
	objc_sync_exit(synchronizedTarget);
}
Copy the code

In this way, both objc_sync_Enter and objc_sync_exit get the same object. But this causes a bug that when you pass nil to @synchronized, no lock is executed, and the state is not thread-safe.

So now we can conclude that

  1. For each callsynchronizedThe OC Runtime assigns a synchronization lock and stores it in the hash table
  2. insynchronizedThis is ok if the object is destroyed or null during execution. But there is no documentation of that conclusion
  3. Don’t putnilPassed to thesynchronized. This will cause your code to become non-thread-safe.