Abstract: Although many programmers can talk a lot about asynchrony, GCD, and other thread-related concepts. But digging deeper, most people are not able to distinguish Race Condition, Atomic, and Immutable objects from their true role in thread safety. So today I’m going to use this article to talk about thread safety as I understand it. Allow me to begin the whole topic Immutable. In recent years, the expression Immutable has become more and more popular

Although many programmers can talk a good game about asynchrony, GCD, and other thread-related concepts. But digging deeper, most people are not able to distinguish Race Condition, Atomic, and Immutable objects from their true role in thread safety.

So today I’m going to use this article to talk about thread safety as I understand it.

Allow me to begin the whole topic Immutable.

Immutable

In recent years, the expression Immutable has become more and more popular. For example, as anyone who has used Swift knows, one of the more obvious changes Swift makes compared to Objective-C is the separation of structs and classes. In some ways, Swift makes a clear distinction between value types and reference types. Why would you do that?

  1. The reference type is not changed after being passed as a parameter and then held by others, which leads to problems that are difficult to troubleshoot.
  2. Provides some degree of thread-safety (because multithreading itself is a major problem in the program due to the uncertainty of writing changes). The advantage of Immutable data is that it cannot be modified once created, so that any thread that uses it is simply reading.

7.19 added:

  1. In Objective-C, Immutable data structures in Swift are thread-safe without a doubt.

  2. For languages like Swift, where value types are designed to be Immutable from the language level, let a = [1, 2, 3, 4, 5] is true Immutable because once a is assigned, it cannot be assigned again. But for Objective-C, let’s say NSArray *a = [NSArray arrayWithObjects:@” NSArray “, nil]; The object to which a refers is indeed immutable, but a itself can be reassigned many times.

So I would like to point out that while the concept of Immutable exists in Objective-C, using Immutable in OC is not directly thread-safe. Otherwise, after using Immutable objects such as NSArray, NSDictionary, etc., Why are there so many weird bugs?

Pointers and Objects

Some people ask why Immutable is not a safe way to transmit a read-only file from one thread to another, even though it makes an object “solid”.

Yes, for an Immutable object, it is Immutable. But in our programs, we always need something to point to our object. What is that thing? Pointer to an object.

Pointers must be familiar to everyone. For Pointers, it’s essentially an object, and when we change the pointer, it’s essentially an assignment to the pointer. So imagine a scenario where you use a pointer to an Immutable object, do you think your pointer changes are thread-safe when multithreading changes? This is why some people are confused when they encounter Immutable objects like NSArray when there are strange bugs in multithreading.

For example:

ImmutableArrayA count 7 self. XXX = self.immutableArrayA; ImmutableArrayB = self. ImmutableArrayB = self.Copy the code

The above code snippet is definitely a thread safety hazard.

7.19 changes:

1. There are deep copies and shallow copies in Objective-C, even if you call one[NSMutableArray Copy]And the resulting NSArray doesn’t mean that all the objects in this array are deep-copied.



2. A copy operation does not mean that it is copied. If the copy object is an IMmutable object, the OC will optimize the copy to retain.

The lock

Given that we think of multithreading to modify Pointers (or objects), it’s natural to think of locks. In today’s iOS blogosphere era, it is well known that NSLock, OSSpinLock and the like can be used as lock protection for ephemeral Critical Section races.

So for multiple threads that need to use shared data sources and support modification operations, such as NSMutableArray adding some objects, we can write the following code:

OSSpinLock ƒ (c ƒ > & _lock); [self.array addObject:@"hahah"]; OSSpinUnlock(&_lock);Copy the code

At first glance, this looks fine, this is the basic write protect lock. If multiple code attempts to add to self.array at the same time, it is added one by one via lock preemption.

But what is the main use of this thing? Atomic locking only solves the Race Condition problem, but it doesn’t solve any logic in your code that requires timing guarantees.

Take this code for example:

if (self.xxx) {
    [self.dict setObject:@"ah" forKey:self.xxx];
}
Copy the code

When you first see code like this, will you think it’s correct? Because self. XXX is non-nil, the key is set in advance, and only if it is non-nil will subsequent instructions be executed. However, the code above is only true if it is single-threaded.

Let’s say that Thread A is currently executing the code above, and when we finish executing if (self.xxx), the CPU switches execution to Thread B, and Thread B calls self.xxx = nil.

Heh heh, the consequence how, presumably I need not say more.


Of course, you could wrap a lock in each getter to determine if the current is nil, but if you do that, your property reading performance will plummet.

Do we have a better solution to this problem? There is an answer, which is to use local variables. For the above code, we make the following changes:

__strong id val = self.xxx;
if (val) {
    [self.dict setObject:@"ah" forKey:val];
}
Copy the code

This way, no matter how many threads try to modify self.xxx, val will essentially remain in its current state, conforming to the non-nil judgment.

Also, using local variables can significantly improve your circular reference problems.

Objective-c Property Setter multithreaded concurrency bug

Finally let’s go back to objective-C, which we use a lot, and talk about problems that come up a lot in real life. Self. XXX = @” KKS “calls the Setter method for XXX. Setter methods are essentially code logic like this:

- (void)setXxx:(NSString *)newXXX {
      if (newXXX != _xxx) {
          [newXXX retain];
          [_xxx release];
          _userName = newXXX;
      }
}
Copy the code

For example, Thread A and Thread B both assign to self. XXX, and both override if (newXXX! = _xxx), [_xxx release] was executed twice, causing the risk of overrelease crash.

Some people say, ha ha, you this is MRC era writing method, I used ARC, no problem.

Ok, so let’s take a look at what the ARC era does, and the underlying source code in Objective-C does this for properties that don’t override setters in ARC (which I believe is the vast majority of the time).

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { id oldValue; // Calculate the offset in the structure id* slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:NULL]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:NULL]; } else {// Some degree of optimization if (*slot == newValue) return; newValue = objc_retain(newValue); } // if (! Atomic) {// oldValue = *slot; // step 2 *slot = newValue; } else { spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)]; _spin_lock(slotlock); oldValue = *slot; *slot = newValue; _spin_unlock(slotlock); } objc_release(oldValue); }Copy the code

Since we generally declare nonatomic objects, the logic goes to the danger zone for the comment above. Again, if we have multiple threads setting A property at the same time, we first get the oldValue from thread A after executing the code in step 1, and then the thread switches to thread B, which also gets the oldValue from step 1, so there are two places holding the oldValue. And then either thread A or thread B will execute to the end of objC_Release (oldValue).

Then, the scene of repeated release appears, crash is waving to you!

If you don’t believe me, try this little example:

for (int i = 0; i < 10000; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        self.data = [[NSMutableData alloc] init];
    });
}
Copy the code

Allocated tokens you can easily see the following error log: Error for object: pointer being freed was not allocated.

conclusion

Having said that, essentially thread safety is an ever-present and relatively difficult problem, and there is no silver bullet. Immutable does not mean that locks can be completely abandoned, nor does it mean that locks are safe. We hope that this article will help you to think more deeply about thread-safety issues, rather than answering locking, using Immutable data, and so on.

Of course, Stick To GCD (dispatch_barrier) is the best solution.

If you find something wrong, please put it forward and make progress together!