This is the 20th day of my participation in the August Text Challenge.More challenges in August
Write in front: iOS underlying principle exploration is my usual development and learning in the accumulation of a section of advanced road. Record my continuous exploration of the journey, I hope to be helpful to all readers.Copy the code
The directory is as follows:
- IOS underlying principles of alloc exploration
- The underlying principles of iOS are explored
- The underlying principles of iOS explore the nature of objects & isa’s underlying implementation
- Isa-basic Principles of iOS (Part 1)
- Isa-basic Principles of iOS (Middle)
- Isa-class Basic Principles of iOS Exploration (2)
- IOS fundamentals explore the nature of Runtime Runtime & methods
- Objc_msgSend: Exploring the underlying principles of iOS
- Slow lookups in iOS Runtime
- A dynamic approach to iOS fundamentals
- The underlying principles of iOS explore the message forwarding process
- Dyld (part 1)
- IOS Basic Principles of application loading principle dyld (ii)
- IOS basic principles explore the loading of classes
- The underlying principles of iOS explore the loading of categories
- IOS underlying principles to explore the associated object
- IOS underlying principle of the wizard KVC exploration
- Exploring the underlying principles of iOS: KVO Principles | More challenges in August
- Exploring the underlying principles of iOS: Rewritten KVO | More challenges in August
- The underlying principles of iOS: Multi-threading | More challenges in August
- GCD functions and queues in iOS
- GCD principles of iOS (Part 1)
- IOS Low-level – What do you know about deadlocks?
- IOS Low-level – Singleton destruction is possible?
- IOS Low-level – Dispatch Source
- IOS bottom – a fence letter blocks the number
- IOS low-level – Be there or be Square semaphore
- IOS underlying GCD – In and out into a scheduling group
- Basic principles of iOS – Basic use of locks
- IOS underlying – @synchronized Flow analysis
Summary of the above column
- Summary of iOS underlying principles of exploration
Sort out the details
- Summary of iOS development details
preface
In the last article, we analyzed the basic use of @synchronized mutex and its internal flow. Next, we will explore the common locks in iOS development. Today, we will start with NSLock mutex flow.
About the use of locks, we have introduced the use of locks in the iOS basic Principles of exploration – basic use.
NSLock – NSRecursiveLock
Starting with a classic case, compare NSLock and NSRecursiveLock;
for (int i= 0; i<1000; i++) {
dispatch_async(dispatch_get_global_queue(0.0), ^ {static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d-%@", value, [NSThread currentThread]);
testMethod(value - 1); }}; testMethod(10);
});
}
Copy the code
Running results:
current value = 10-<NSThread: 0x60000154d200>{number = 6, name = (null)}
current value = 10-<NSThread: 0x60000150d840>{number = 5, name = (null)}
current value = 10-<NSThread: 0x60000154c8c0>{number = 9, name = (null)}
current value = 10-<NSThread: 0x600001527800>{number = 12, name = (null)}
current value = 10-<NSThread: 0x600001560b80>{number = 11, name = (null)}
current value = 10-<NSThread: 0x600001551280>{number = 7, name = (null)}
current value = 10-<NSThread: 0x600001519480>{number = 4, name = (null)}
current value = 10-<NSThread: 0x60000155c000>{number = 3, name = (null)}
current value = 10-<NSThread: 0x600001527e40>{number = 8, name = (null)}
current value = 9-<NSThread: 0x60000150d840>{number = 5, name = (null)}
current value = 9-<NSThread: 0x60000154c8c0>{number = 9, name = (null)}
current value = 10-<NSThread: 0x600001548200>{number = 14, name = (null)}
current value = 9-<NSThread: 0x60000154d200>{number = 6, name = (null)}
current value = 9-<NSThread: 0x600001527800>{number = 12, name = (null)}
current value = 9-<NSThread: 0x600001560b80>{number = 11, name = (null)}
...
current value = 2-<NSThread: 0x600001564080>{number = 33, name = (null)}
current value = 3-<NSThread: 0x60000150ef40>{number = 28, name = (null)}
current value = 1-<NSThread: 0x600001564080>{number = 33, name = (null)}
current value = 2-<NSThread: 0x60000150ef40>{number = 28, name = (null)}
current <NSThread: 0x600001564080>{number = 33, name = (null)}
current value = 1-<NSThread: 0x60000150ef40>{number = 28, name = (null)}
current <NSThread: 0x60000150ef40>{number = 28, name = (null)}
current value = 2-<NSThread: 0x60000150f380>{number = 40, name = (null)}
current <NSThread: 0x6000015740c0>{number = 31, name = (null)}
current value = 1-<NSThread: 0x60000150f380>{number = 40, name = (null)}
current <NSThread: 0x60000150f380>{number = 40, name = (null)}
Copy the code
Run directly, you can see that it’s out of order, because of multithreading, plus recursive operations can have this problem.
In order to print sequentially, we have to think about locking, thinking about putting locks there to do what we want.
A brief analysis shows that the core code of our business is testMethod(10); So we use NSLock or NSRecursiveLock above and below this line respectively to lock and unlock.
It is important to note that the lock is usually placed in the code block. This will cause the lock to be placed at the beginning of the block execution, but it will not be unlocked, so there is a problem.
NSLock is a non-recursive lock, so there’s a problem with recursively locking. If we try NSRecursiveLock, we can see that it prints the entire lock 10-0, not 1000 times, so even though NSRecursiveLock is recursive, But recursive use of multiple threads can also be problematic.
current value = 10-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 9-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 8-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 7-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 6-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 5-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 4-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 3-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 2-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 1-<NSThread: 0x600000211440>{number = 6, name = (null)}
Copy the code
At this point, I think of the iOS bottom – @synchronized process analysis @synchronized this lock, can be multithreaded recursive operation. So, we use @synchronized locks for our business in blocks, which makes sequential printing perfect.
The underlying implementation
In the OC environment, using NSLock, we can see by clicking:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
Copy the code
In fact, this is a protocol from NSObject for NSLocking, so as long as we implement this protocol we will have these two methods (many locks will have these two methods). But we don’t see implementation of either method. Below, we combine swift source code, look at the concrete implementation of these two methods.
NSLock
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init(){#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
open func lock(){#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock(){#if os(Windows)
ReleaseSRWLockExclusive(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
Copy the code
NSRecursiveLock
internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
super.init()
#if os(Windows)
InitializeCriticalSection(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
#if CYGWIN
var attrib : pthread_mutexattr_t? = nil
#else
var attrib = pthread_mutexattr_t()
#endif
withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
open func lock(){#if os(Windows)
EnterCriticalSection(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock(){#if os(Windows)
LeaveCriticalSection(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
Copy the code
conclusion
As you can see, the bottom layer of NSLock and NSRecursiveLock encapsulates the lock pthread_mutex_lock. But why is NSRecursiveLock recursive? Because at initialization, pthread_mutexattr_setType (attrs, Int32(PTHREAD_MUTEX_RECURSIVE)) has a setType operation, identified.
NSCondition
The object of the NSCondition actually acts as a lock and a thread inspector: the lock master performs tasks that are triggered by the condition in order to protect the data source when detecting the condition; The thread checker mainly determines whether to continue running a thread and whether a thread is blocked based on conditions.
- [Condition lock] is generally used for multiple threads to access and modify the same data source at the same time to ensure that the data source can be accessed and modified only once at the same time
lock
Wait untilunlock
, can be accessed. - [condition unlock] Is used with lock.
- [condition wait] makes the current thread wait.
- [Condition signal] The CPU sends a signal telling the thread to stop waiting and continue executing.
Production and consumption model
Let’s explain the use of NSCondition through a producer-consumer model.
Producers are responsible for making tickets and consumers are responsible for buying them. We use ticketCount to represent the current number of tickets. Production is ticketCount+1; The consumer is tichekCount-1 (waiting for tickets when ticketCount is 0); As follows:
- producers
self.ticketCount = self.ticketCount + 1;
NSLog(@"Produce an existing count %zd",self.ticketCount);
Copy the code
- consumers
if (self.ticketCount == 0) {
NSLog(@"Wait for count %zd",self.ticketCount);
return;
}
// Pay attention to the consumption behavior after waiting for the judgment of the condition
self.ticketCount -= 1;
NSLog(@"Consume one left count %zd",self.ticketCount);
Copy the code
Similarly, in a multi-threaded situation where we have both tickets and tickets, there will be confusion in the data count (let’s analyze some of the logs) :
Produce an existing count1Wait for the count0Consume one with count left0Produce an existing count1Produce an existing count2Consume one with count left1Consume one with count left0Produce an existing count1Consume one with count left1Produce an existing count2Produce an existing count2Consume one with count left0Consume one with count left1
Copy the code
After production, one ticket is waiting because the log shows that there are 0 tickets, and there are 0 tickets left after consumption, then one was produced and one was consumed and the log shows that there are 1 tickets left. This is the mess of data in multiple threads. First of all, when you make a ticket, we need to add a lock:
[condition lock]; // Add a lock to prevent multithreaded data from being unsafe
self.ticketCount = self.ticketCount + 1;
NSLog(@"Produce an existing count %zd",self.ticketCount);
[condition unlock];/ / unlock
[condition signal];// Send a signal
Copy the code
To buy a ticket, we lock it like this:
[condition lock]; // Add a lock to prevent multithreaded data from being unsafe
if (self.ticketCount == 0) {
NSLog(@"Wait for count %zd",self.ticketCount);
[condition wait]; // Wait for tickets to be made
}
// Pay attention to the consumption behavior after waiting for the judgment of the condition
self.ticketCount -= 1;
NSLog(@"Consume one left count %zd",self.ticketCount);
[condition unlock]; / / unlock
Copy the code
In this case, the log is as follows:
Produce an existing count1Consume one with count left0Wait for the count0Produce an existing count1Produce an existing count2Consume one with count left1Consume one with count left0Produce an existing count1Produce an existing count2Produce an existing count3Produce an existing count4Produce an existing count5Produce an existing count6Produce an existing count7Produce an existing count8Consume one with count left7Produce an existing count8Produce an existing count9Produce an existing count10Consume one with count left9Produce an existing count10Consume one with count left9Produce an existing count10
Copy the code
Perfect.
The underlying implementation
As with NSLock and NSRecursiveLock above, this is an encapsulation of pthread_mutex_lock, but with additional conD handling.
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
public override init(){#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(cond)
#else
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
#endif
}
deinit {
#if os(Windows)
// SRWLock do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
#endif
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}
open func lock(){#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock(){#if os(Windows)
ReleaseSRWLockExclusive(mutex)
#else
pthread_mutex_unlock(mutex)
#endif
}
open func wait(){#if os(Windows)
SleepConditionVariableSRW(cond, mutex, WinSDK.INFINITE, 0)
#else
pthread_cond_wait(cond, mutex)
#endif
}
open func wait(until limit: Date) -> Bool {
#if os(Windows)
return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
#else
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
#endif
}
open func signal(){#if os(Windows)
WakeConditionVariable(cond)
#else
pthread_cond_signal(cond)
#endif
}
Copy the code
NSConditionLock
NSConditionLock is a lock, and once one thread has acquired the lock, the other threads must wait.
- Conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock Until another thread is unlocked.
- [conditionlock lockWhenCondition: A condition] said if there are no other thread to acquire the lock, but the internal condition of the lock is not equal to A condition, it still cannot obtain the lock, and still waiting. If the internal condition is equal to condition A, and no other thread acquires the lock, the code area is entered, while it is set to acquire the lock, and any other thread will wait for its code to complete until it unlocks.
- [conditionlock unlockWhenCondition: A condition] said lock is released, at the same time, the internal condition is set to A condition.
- Return [conditionlock lockWhenCondition: A conditional beforeData: A time] specifies that the thread is not blocked if the lock is not acquired and the time is exceeded. Note, however, that the return value is NO, which does not change the state of the lock. The purpose of this function is to allow processing in both states.
sample
In the same way, conditionlock and NSConditionLock make conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@Thread 1 "");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@Thread 2 "");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0.0), ^{
[conditionLock lock];
NSLog(@"Thread 3");
[conditionLock unlock];
});
Copy the code
At this point, why don’t you guess what the print order is?
Okay, let’s see, because when Condition initializes, it passes a 2; So 2 and 3 will be executed first, but since task 2 has a delayed operation, there is a high probability that task 3 will be executed first, then task 2, and then task 1 will be executed last; Run the project and the result is the same:
thread3thread2thread1
Copy the code
After analyzing the execution process, I could not help wondering:
- What does NSConditionLock have to do with NSCondition?
- InitWithCondition :2, what is 2 here?
- How are lockwhenconditions controlled?
- What does the lock with condition do?
Here, with these four questions, we try to explore the internal processes and answer the above four questions by breaking debugging:
When the code is executed to initialize NSConditionLock, execute initWithCondition and find that the symbol breakpoint is not stuck. It is normal that the breakpoint is not stuck, because there is a problem with the symbol, so let’s change it:
Then rerun: Stuck breakpoint,
The project is running on my machine, and we can register read the contents of the register:
The most important detail is:
- X0 (Receiver of message)
- X1 (current SEL)
- X2 (method parameter)
-[NSConditionLock initWithCondition:]: process analysis
And when I print it out, it matches all of our methods.
So in this case, because it’s assembly code, in the process of processing, we need to sift through and extract the execution statements that capture the focus. What’s the focus? Bl do jump related processing statement.
So, the bl statement in the assembly code all hit the breakpoint, we continue to follow the process.
Then proceed down:
Then execute (roughly the operation of memory opening) :
Continue down:
Continue to:
And then we go to ret, return return;
Look at the above process, is nothing more than some initialization, open up memory space. We see that the object returned is an object of NSConditionLock (condition = 2) that has a member variableNSCondition
和 2
.
Next, we continue to look at lockwhenconditions:
-[NSConditionLock lockWhenCondition:]: process analysis
Moving on, we’re at the first BL: it’s an NSDate calling the distantFuture method.
Let’s not talk about why Date is called, let’s first go through the current process to see what kind of process it is. Continue to:
NSConditionLock call lockWhenCondition: beforeDate: method, the second parameter is: _NSConstantDateDistantFuture
Here to call lockWhenCondition: beforeDate: method, so, we make a symbol on the breakpoint:
Enter lockWhenCondition: beforeDate: after that, continue to execute, see the NSCondition object call lock method:
Returns a 1 (no more waiting) :
Finally, there is an unlock operation.
-[NSConditionLock unlockWithCondition:]: process analysis
The symbolic breakpoint has been added, and executes directly down:
There was a broadcast
Finally, when it returns, the NSCondition object does unlock
The source code control
Finally, we use the source code, compared with the process to explore the analysis, to see if the same:
NSConditionLock
There is an internal NSCondition member variable that will be assigned when initialized, which explains the initialization of conditionLock in NSConditionLock.
open class NSConditionLock : NSObject.NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_cond.broadcast()
_cond.unlock()
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while_thread ! = nil || _value ! = condition {if! _cond.wait(until: limit) { _cond.unlock()return false}} #if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
open func unlock(withCondition condition: Int) {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_value = condition
_cond.broadcast()
_cond.unlock()
}
}
Copy the code
In the lockWhenCondition, there is a date. distantfuture. this explains that in the process, the NSDate we see calls distantFuture
After locking, compare the relevant operations in if! _cond.wait(until: limit) will call wait, and if time out, will unlock and return false. Thread will be processed, and it will return true, which means that we returned 1 in our exploration process.
The final unlock, condition and broadcast unlock, the same as the final exploration process.
conclusion
This article ends with the use of locks. In fact, in actual use, any lock can be used, depending on the specific business, the use of which lock will be more appropriate, more efficient. After exploring this article, we have become a little clearer about the use of locks in development. I hope I can help you understand the lock. Come on everybody!!