Multithreading is an unavoidable topic in software programming. The use of multithreading can improve the running efficiency of programs, but it also brings a new problem: how to ensure thread safety?

When a function or library is called in a multi-threaded environment, it can correctly handle the shared variables between multiple threads, so that the program functions can be completed correctly. In other words, a variable is “consistent” as long as it is accessed by a thread. This “consistent” means that the variable does not change from the time the thread starts accessing it to the time it ends accessing it.

Keeping a variable thread-safe, then, can be interpreted as keeping a variable consistent over a particular period of time. This particular time can be understood as thread-safe atomicity granularity, as described below.

example

On iOS, you’ll often see the following code example:

@property (atomic, assign) int num; // thread Afor (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread A: %d\d ",self.num);
}

// thread B
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread B: %d\d ",self.num);
}Copy the code
@property (atomic, strong) NSString * stringA; //thread Afor (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}

//thread B
for (int i = 0; i < 10000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}Copy the code

Example A may not end up with 20000, but example B may crash. These two examples illustrate a problem: property plus the atomic keyword does not necessarily guarantee thread-safe properties.

Atomic granularity for thread safety

So why does the use of the atomic keyword not guarantee thread-safe property for the scenario above?

The atomic keyword locks the read and write operations on the property, in other words, the Setter/Getter operations on the property. However, the atomic keyword only guarantees that the current keyword can be read or written by at most one thread at a time.

Self. num = self.num + 1; This contains three operations: read num through the Getter, increment num, and write the result back to num. The atomic keyword guarantees that every operation is atomic. However, atomic cannot guarantee that the properties will not be accessed by other threads during the time between each operation. After TheadA incremented num by 1, the CPU time was allocated to Thread B. Thread B may have modified num. When the CPU time was allocated back to Thread A, the num+1 might not be the original num+1. Num +1 = num+1 = num+1 = num+1 = num+1 = num+1 The problem with using atomic is that it has no effect if multithreaded access to attributes is done directly through Ivar, without calling getters/setters.

When executing self.stringa. length >= 10 and assuming that stringA is “a very long string”, Thread A is switched to Thread A. Thread A changes stringA to string. CPU time allocated to the Thread B again at this moment, at this time the Thread B will perform [self. StringA substringWithRange: NSMakeRange (0, 10)]. The current stringA value has been changed to “string” by Thread A, so the string access will be out of bounds and crash.

The problem with examples 1 and 2 is that while each read or write to the string is safe, there is no guarantee that the combined operation of individual threads is safe, which is a thread-safe atomic granularity problem. Atomic granularity is Getter/Setter, but operations on multiple lines of code do not guarantee atomicity. A better solution for examples 1 and 2 is to use a locking mechanism.

// thread A [_lock lock];for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread A: %d\d ",self.num);
}
[_lock unlock];

// thread B
[_lock lock];
for (int i = 0; i < 10000; i++) {
    self.num = self.num + 1;
    NSLog(@"Thread B: %d\d ",self.num);
}
[_lock unlock];Copy the code
//thread A [_lock lock];for (int i = 0; i < 10000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];

//thread B
[_lock lock];
for (int i = 0; i < 10000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}
[_lock unlock];Copy the code

After the code is locked, only the thread that locks the code can access the lock code, which ensures that the lock code will not be executed by other threads, thus ensuring thread safety from a larger granularity. If locking is used to control code-level atomic granularity, there is no need to use smaller atomic granularity. For large-grained atomic has been able to ensure the security of related business code thread, if add much smaller atomic control of particle size, which will be redundant, and atomic is a more granular locking mechanism, there are quite a few on performance, so in general if you use a larger granularity atomicity, there is no need to use a smaller granularity of atomicity, There is no need to use atomic for attribute variables in locked code.

Tips for not locking

For example 2, if you don’t lock, how can you guarantee that the code won’t crash?

5 / / examplesfor (int i = 0; i < 10000; i ++) {
    NSString *immutableTempString = self.stringA;
    if(immutableTempString.length >= 10) { NSString* subStr = [immutableTempString substringWithRange:NSMakeRange(0, 10)]; }}Copy the code

The reason for the crash in example 2 is that the memory region to which stringA points has changed and is accessed out of bounds. But that’s not the case in example 5, because in example 5 we use the temporary variable immutableTempString, which points to the memory space before stringA changes, and when stringA changes the memory it points to, Since the memory that stringA refers to is pointed to by immutableTempString, it will not be reclaimed by the system for now. When [immutableTempString substringWithRange: NSMakeRange (0, 10)] called, immutableTempString refers to the value or the original stringA, so the crash will not occur. The principle of this method is that by using a temporary variable to hold the original changes the value of the former, all the operation of the temporary variable pointing to the value of the operation, rather than directly using attributes to values, it can guarantee the value of the variable context scenarios is consistent, and because the variables are temporary, so will only be visible to the current thread, is not visible to other threads, Thus, thread safety is guaranteed to some extent.

conclusion

In iOS, it is not easy to assume that adding the atomic keyword will guarantee thread-safe properties. In practice, however, due to the complexity of business code, locking controls with greater granularity than atomic are used in most cases. With larger-grained locks, the use of atomic is no longer necessary for performance and necessity reasons. In some cases, if locking is not possible and you want to ensure that the code does not crash, you can use temporary variables to point to original values to ensure a degree of thread-safety.

All in all, thread safety with multiple threads is a complex issue, and it is best to avoid multithreaded design as much as possible

Reference

What’s wrong with iOS multithreading?