11 types of locks and performance comparison in iOS development

11 types of locks and performance comparison in iOS development
[[221143]]

In daily development, we often use multithreading. Multithreading brings us great convenience and improves the execution efficiency of the program, but it also brings data race. The definition of data race is very simple: when at least two threads access the same variable at the same time, and at least one of them is a write operation, a data race occurs. Therefore, some synchronization mechanisms must be used to ensure the accuracy of the data. Locks are one of the synchronization mechanisms.

How to detect Data race in a project?

Just check Thread Sanitizer in the settings, and by the way, you can check Pause on issues to breakpoint to the corresponding code.

Let's get down to business and talk briefly about locks in iOS and related content (due to my limited ability, there are inevitably some omissions or errors in the article, please feel free to give me your advice! Thank you!)

Simple performance test

The following figure is the result of my own test on the lock in iOS. The numbers in the figure represent the time required for each lock and unlock, in nanoseconds. The code is here, and the code is based on YY's no longer secure OSSpinLock, which is basically the same as YY's figure??, YY's unit is μs, which should be 1000 times, or maybe it's wrong~


LockPerformance.jpg

Note: The running phone is: iPhone 6s plus, system version: 11.2.2, Xcode 9.2; the unit of the number is ns (the specific value obtained is the average of multiple runs).

It is worth noting that: 1. This number only represents the time taken to unlock each time, and cannot fully represent the performance. 2. Different models and systems, different number of cycles may result in slightly different results.

But it can still be seen that @synchronized: has the worst performance.

Before we talk about these locks in detail, let's first talk about some conceptual definitions: (refer to Wikipedia)

  1. Critical section: refers to a piece of code that accesses public resources, not a mechanism or algorithm.
  2. Spin lock: A lock used for multi-thread synchronization. The thread repeatedly checks whether the lock variable is available. Since the thread keeps executing during this process, it is a busy wait. Once the spin lock is acquired, the thread will keep the lock until the spin lock is explicitly released. Spin locks avoid the scheduling overhead of the process context, so they are effective for situations where the thread will only be blocked for a short time.
  3. Mutex: A mechanism used in multithreaded programming to prevent two threads from reading and writing the same common resource (such as a global variable) at the same time. This purpose is achieved by slicing the code into critical sections one by one.
  4. Read-write lock: It is a synchronization mechanism for concurrent control of computer programs, also known as "shared-mutex lock" and multi-reader-single-writer lock) used to solve the problem of multi-threaded reading and writing of common resources. Read operations can be reentered concurrently, and write operations are mutually exclusive. Read-write locks are usually implemented with mutex locks, condition variables, and semaphores.
  5. Semaphore: It is a more advanced synchronization mechanism. Mutex can be said to be a special case of semaphore when it only takes the value 0/1. Semaphore can have more value spaces and is used to achieve more complex synchronization, not just mutual exclusion between threads.
  6. Condition lock: It is a condition variable. When some resource requirements of the process are not met, it goes into sleep, that is, locked. When the resources are allocated, the condition lock is opened and the process continues to run.

Mutex

1.NSLock: It is a lock exposed to developers in the form of an object in the Foundation framework (the Foundation framework also provides NSConditionLock, NSRecursiveLock, and NSCondition).NSLock is defined as follows:

  1. @protocol NSLocking
  2. - (void)lock;
  3. - (void)unlock;
  4. @ end  
  5. @interface NSLock : NSObject <nslocking> {
  6. @private
  7. void *_priv;
  8. }
  9. - (BOOL)tryLock;
  10. - (BOOL)lockBeforeDate:(NSDate *)limit;
  11. @property (nullable, copy) NSString * name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
  12. @ end </nslocking>

Both tryLock and lock methods request locks. The only difference is that trylock can continue to do some tasks and processing when the lock is not obtained. The lockBeforeDate method is also relatively simple, which is to obtain the lock before the limit time point, and return NO if it is not obtained.

In the actual project: NSLock is used in AFURLSessionManager.m of AFNetworking as follows:

  1. - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
  2. ...
  3. self.lock = [[NSLock alloc] init];
  4. self.lock.name = AFURLSessionManagerLockName;
  5. ...
  6. }
  7. - (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
  8. forTask:(NSURLSessionTask *)task
  9. {
  10. ...
  11. [self.lock lock];
  12. self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
  13. [delegate setupProgressForTask:task];
  14. [self addNotificationObserverForTask:task];
  15. [self.lock unlock];
  16. }

2.pthread_mutex:

In actual projects: You can see this in YYMemoryCach of YYKit

  1. - (instancetype)init {
  2. ...
  3. pthread_mutex_init(&_lock, NULL );
  4. ...
  5. }
  6. - (void)_trimToCost:(NSUInteger)costLimit {
  7. BOOL finish = NO ;
  8. pthread_mutex_lock(&_lock);
  9. if (costLimit == 0) {
  10. [_lru removeAll];
  11. finish = YES;
  12. } else if (_lru->_totalCost <= costLimit) {
  13. finish = YES;
  14. }
  15. pthread_mutex_unlock(&_lock);
  16. if (finish) return ;
  17.       
  18. NSMutableArray *holder = [NSMutableArray new];
  19. while (!finish) {
  20. if (pthread_mutex_trylock(&_lock) == 0) {
  21. if (_lru->_totalCost > costLimit) {
  22. _YYLinkedMapNode *node = [_lru removeTailNode];
  23. if (node) [holder addObject:node];
  24. } else {
  25. finish = YES;
  26. }
  27. pthread_mutex_unlock(&_lock);
  28. } else {
  29. usleep(10 * 1000); //10 ms
  30. }
  31. }
  32. ...
  33. }

3.@synchronized:

In the actual project: getter method of isNetworkActivityOccurring attribute in AFNetworking

  1. - (BOOL)isNetworkActivityOccurring {
  2. @synchronized(self) {
  3. return self.activityCount > 0;
  4. }
  5. }

Spin lock

1.OSSpinLock:

  1. OSSpinLock lock = OS_SPINLOCK_INIT;
  2. OSSpinLockLock(&lock);
  3. ...
  4. OSSpinLockUnlock(&lock);

The above is how to use OSSpinLock. The compiler will report a warning that it has been deprecated. OSSpinLock is no longer used because it is no longer safe in some scenarios. You can refer to YY master's no longer safe OSSpinLock. In the Protocol Buffers project, you can see such comments. Everyone has replaced it with a new solution.

  1. // NOTE: OSSpinLock may seem like a good fit here but Apple engineers have
  2. // pointed out that they are vulnerable to live locking on iOS in cases of  
  3. // priority inversion:
  4. // http://mjtsai.com/blog/2015/12/16/osspinlock- is -unsafe/
  5. // https://lists.swift.org/pipermail/swift-dev/Week- of -Mon-20151214/000372.html

2.os_unfair_lock:

os_unfair_lock is the solution officially recommended by Apple to replace OSSpinLock, but it can only be called in systems above iOS10.0.

  1. os_unfair_lock_t unfairLock;
  2. unfairLock = &(OS_UNFAIR_LOCK_INIT);
  3. os_unfair_lock_lock(unfairLock);
  4. os_unfair_lock_unlock(unfairLock);

Read-write lock

As mentioned above, the read-write lock is also called a shared-mutex lock.

pthread_rwlock:

  1. //Add read lock
  2. pthread_rwlock_rdlock(&rwlock);
  3. //Unlock
  4. pthread_rwlock_unlock(&rwlock);
  5. //Add write lock
  6. pthread_rwlock_wrlock(&rwlock);
  7. //Unlock
  8. pthread_rwlock_unlock(&rwlock);

Recursive Lock

A feature of recursive locks is that the same thread can be locked N times without causing deadlock.

1.NSRecursiveLock:

NSRecursiveLock is used in YYWebImageOperation.m in YYKit:

  1. _lock = [NSRecursiveLock new];
  2. - (void)dealloc {
  3. [_lock lock];
  4. ...
  5. ...
  6. [_lock unlock];
  7. }

2.pthread_mutex(recursive):

The pthread_mutex lock also supports recursion, just set PTHREAD_MUTEX_RECURSIVE

  1. pthread_mutex_t lock;
  2. pthread_mutexattr_t attr;
  3. pthread_mutexattr_init(&attr);
  4. pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
  5. pthread_mutex_init(&lock, &attr);
  6. pthread_mutexattr_destroy(&attr);
  7. pthread_mutex_lock(&lock);
  8. pthread_mutex_unlock(&lock);

Conditional Lock

1. NSCondition:

definition:

  1. @interface NSCondition : NSObject <nslocking> {
  2. @private
  3. void *_priv;
  4. }
  5. - (void)wait;
  6. - (BOOL)waitUntilDate:(NSDate *)limit;
  7. - (void)signal;
  8. - (void)broadcast;</nslocking>

Follow the NSLocking protocol. When used, it is also lock, unlock and unlock. Wait is to wait stupidly. The waitUntilDate: method is to wait for a while, which will block the thread. Signal is to wake up a waiting thread, and broadcast is to broadcast and wake up all.

  1. NSCondition *lock = [[NSCondition alloc] init];
  2. //Son thread
  3. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  4. [lock lock];
  5. while ( No Money ) {
  6. [lock wait];
  7. }
  8. NSLog(@ "The money has been used up." );
  9. [lock unlock];
  10. });
  11.       
  12. //Father thread
  13. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  14. [lock lock];
  15. NSLog(@ "Work hard to make money." );
  16. [lock signal];
  17. [lock unlock];
  18. });

2.NSConditionLock:

definition:

  1. @interface NSConditionLock : NSObject <nslocking> {
  2. @private
  3. void *_priv;
  4. }
  5. - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
  6. @property (readonly) NSInteger condition;
  7. - (void)lockWhenCondition:(NSInteger)condition;
  8. - (BOOL)tryLock;
  9. - (BOOL)tryLockWhenCondition:(NSInteger)condition;
  10. - (void)unlockWithCondition:(NSInteger)condition;
  11. - (BOOL)lockBeforeDate:(NSDate *)limit;
  12. - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;</nslocking>

It’s very simple, the method is very clear, and it’s basically the same as above.

Semaphore

dispatch_semaphore:

dispatch_semaphore is used in YYThreadSafeArray.m in YYKit, and YY master has such a comment:

  1. @discussion Generally, access performance is   lower than NSMutableArray,  
  2. but higher than using @synchronized, NSLock, or pthread_mutex_t.
  3. 3#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \  
  4. __VA_ARGS__; \  
  5. dispatch_semaphore_signal(_lock);

Summarize:

In fact, this article is all about the most basic content. When reading some open source projects, you will often encounter some ways to keep threads synchronized. Because the selection may be different in different scenarios, this article will just make a simple record~ I believe that after reading this article, you should be able to choose the right lock according to different scenarios, and be able to tell the difference between spin locks and mutex locks.

at last:

Due to my limited ability, there are inevitably some omissions or errors in the article. Please feel free to give me your advice! Thank you! At the same time, if you have any questions about locks, you can leave a message, communicate together, and make progress together~?? I hope everyone can make a little progress every day~

<<:  iOS 11–11.1.2 full jailbreak released: much better this time

>>:  The detailed schedule of the Google Developer Conference has been confirmed: the new version of Android is coming!

Recommend

“FaceApp” explosive operational growth strategy!

In recent days, I believe everyone has been flood...

They dare to squeeze into any narrow place. Are cats actually liquid?

Leviathan Press: Cat owners know the flexibility ...

iQIYI VIP price increases again, why?

iQiyi has raised its prices again. From 0:00 on D...

How to block IP in Baidu bidding and deal with invalid clicks?

When doing Baidu bidding, you must deal with mali...

“Experts are advised not to give advice”, does it really make sense?

Not long ago, #It is recommended that experts do ...

Tesla chairman talks about Musk: I don't think he faces any challenges

Last fall, the Securities and Exchange Commission...

Why is the upcoming Year of the Tiger only 355 days?

The upcoming Lunar Year of the Tiger From Februar...

The concerns and worries behind Xiaomi's "no listing within five years"

As the news of Alibaba's successful listing i...

ATET A8 Bluetooth controller with customized appearance review

As one of the control standards for many games, t...