preface

Learning summary of various locks in iOS for future reference, if there is any problem, please help to correct

primers

@property (nonatomic, strong) *foo is one of the most common declarations we use in daily development. We know the difference between atomic and nonatomic properties.

  • atomic: atomicity, this property is default, through thesetter,getterLocking ensures data read and write security
  • nonatomic: nonatomic, that is, unlocked. The advantage is that speed is better than useatomicMost scenarios will not have problems

As a compiler identifier, @Property helps us to quickly generate member variables and their getters/setters. It also helps us to manage complex transactions such as memory and security through property keywords. So how does atomic help us ensure that member variables can be read and written safely? Let’s look at some code:

//@property(retain) UITextField *userName; - (UITextField *) userName {UITextField *retval = nil; @synchronized(self) { _userName = retval; }return retval;
}
- (void) setUserName:(UITextField *)userName { @synchronized(self) { [_userName release]; _userName = [userName retain]; }}Copy the code

We can easily see that the compiler uses the lock to ensure that the current _userName variable is read and written safely, so as not to generate dirty data. This is what the compiler does for us. In fact, if we dig a little deeper, it’s not really @synchronized(object).

The spin lock does not cause the thread state to change. It is always in user state, that is, the thread is always active. The thread will not enter the blocking state (sleep), reduce unnecessary context switch, fast execution

When a non-spin lock cannot be acquired, it will enter the blocking state and thus enter the kernel state. When the lock is acquired, it needs to recover from the kernel state and switch the thread context, which affects the lock performance

Why atomic is the default attribute? We can see that Apple’s design is to tell us that efficiency is worth security in many cases

How to use the lock

Consider the output in a simple piece of code

- (void)unlockTest {
    NSMutableString *string = [@"Mike" mutableCopy];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [string appendString:@"-Locked"];
        NSLog(@"% @",string);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [string appendString:@"-JailBreaked"];
        NSLog(@"% @",string);
    });
}  
Copy the code

The purpose of writing this code is to use the variable after changing it in different threads

Console output:

2019-11-11 16:52:43.019128+0800 DiscoverLock_iOS[89763:11225442] Mike- Locked-jailBreaked 2019-11-11 16:52:43.019128+0800  DiscoverLock_iOS[89763:11225441] Mike-Locked-JailBreakedCopy the code

This is obviously not what we want, so how do we make sure that the variables that we use in different threads are the values that we want? One answer is to lock

NSLocking

OC offers four following classes, respectively is NSLock/NSCondtionLock/NSRecursiveLock/NSCondition, meet the needs of object-oriented programming

@protocol NSLocking - (void)lock; // Block thread, sleep thread - (void)unlock; @endCopy the code

The basic process of locking: [lock] -> [operation] -> [unlock] The four classes mentioned above can achieve this basic function, which will not be described in the following

- (void)lockedTest {
    NSMutableString *string = [@"Mike"mutableCopy]; Dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{// [lock]; [string appendString:@"-Locked"];
        NSLog(@"% @",string); / / / lock unlock; }); Dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{// [lock]; [string appendString:@"-JailBreaked"];
        NSLog(@"% @",string); / / / lock unlock; }); }Copy the code

Console output:

DiscoverLock_iOS[90562:11303793] Mike-Locked
DiscoverLock_iOS[90562:11303799] Mike-Locked-JailBreaked
Copy the code
DiscoverLock_iOS[90562:11303793] Mike-JailBreaked
DiscoverLock_iOS[90562:11303799] Mike-JailBreaked-Locked
Copy the code

NSConditionLock (DISPATCH_QUEUE_PRIORITY) does not guarantee the order of execution of DISPATCH_QUEUE_PRIORITY. This is a thread synchronization class

NSLock

  • - (BOOL)tryLock;: Attempts to lock, returns NO on failure, does not block the thread
  • - (BOOL)lockBeforeDate:(NSDate *)limit;: Attempts to lock before the specified time. If NO is returned, the thread is blocked before the specified time

Sample code:

- (void)lockTest {

    NSMutableString *string = [@"Mike" mutableCopy];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    LOCK(
         [string appendString:@"-Locked"];
         NSLog(@"% @",string);
         sleep(5);
        )
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    TRYLOCK(
            [string appendString:@"-UnLock"];
            NSLog(@"% @",string);
            sleep(3);
        )
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    TRYLOCKINDURATION(2,
                      [string appendString:@"-Ending"];
                      NSLog(@"% @",string); ) ; NSLog(@"- = - = - = - = -");
    });
}
Copy the code

Console output:

2019-11-11 19:54:08.807763+0800 DiscoverLock_iOS[92986:11465678] Locked 2019-11-11 19:54:08.807763+0800 DiscoverLock_iOS[92986:11465679] Trylock-no 2019-11-11 19:54:08.807889+0800 DiscoverLock_iOS[92986:11465679] Mike-Locked-UnLock 2019-11-11 19:54:10.810165+0800 DiscoverLock_iOS[92986:11465677] trylockbefore-no 2019-11-11 19:54:10.810523+0800 DiscoverLock_iOS[92986:11465677] Mike- locked-unlock -Ending 2019-11-11 19:54:10.810810+0800 DiscoverLock_iOS[92986:11465677] -=-=-=-=-Copy the code

As you can see from the sample code output above, – (BOOL)tryLock; – (BOOL)lockBeforeDate (NSDate *)limit; – (BOOL)lockBeforeDate (NSDate *)limit; Blocks the thread operation before the time is up, and after waiting for the appropriate time, returns NO and executes the next print, obviously blocking the thread while it waits

The macro definitions used in the above code suggest that when using locks in the future, try to keep a clear head or simply define some convenient methods to ensure that [lock] – [unlock] pairs occur, to avoid thread blocking or deadlock situations

#define LOCK(...) \
[_lock lock]; \
__VA_ARGS__; \
[_lock unlock]; \

#define TRYLOCK(...) \
BOOL locked = [_lock tryLock]; \
NSLog(@"% @",locked? @"TryLock-YES": @"TryLock-NO"); \
__VA_ARGS__; \
if (locked) [_lock unlock]; \

#define TRYLOCKINDURATION(duration,...) \
BOOL locked = [_lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:duration]]; \
NSLog(@"% @",locked? @"TryLockBefore-YES": @"TryLockBefore-NO"); \
__VA_ARGS__; \
if (locked) [_lock unlock]; \
Copy the code

NSConditionLock

  • - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;: convenience constructor that passes in the initial value of the conditional lock
  • @property (readonly) NSInteger condition;: Specifies the value of the current conditional lock
  • - (void)lockWhenCondition:(NSInteger)condition;: Performs the following operation when the conditional value of the lock is equal to the value passed in, otherwise the thread blocks
  • - (BOOL)tryLock;: Attempts to lock, returns NO on failure, does not block the thread
  • - (BOOL)tryLockWhenCondition:(NSInteger)condition;: Attempts to lock. If the condition value of the lock is equal to the value passed in, the lock succeeds. Otherwise, NO is returned and the thread is not blocked
  • - (void)unlockWithCondition:(NSInteger)condition;: Unlock operation and change the conditional value of the lock to the passed value
  • - (BOOL)lockBeforeDate:(NSDate *)limit;: Attempts to lock before the specified time. If NO is returned, the thread is blocked before the specified time
  • - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;When the conditional value of the lock is equal to the value passed in, YES is returned if the lock is successful, otherwise NO is returned if the lock fails, and the thread is blocked before the time

Condition conditionlock – (void)lockWhenCondition:(NSInteger)condition; If condition is equal to condition at initialization, lock will be able to lock correctly. The – (void) unlockWithCondition (NSInteger) condition; Condition conditionlock is the same as NSLock if condition conditionlock is not used

We mentioned thread synchronization above, so let’s take a look at the following code

- (void)conditionLockUnordered {
    NSMutableString *conditionString = [[NSMutableString alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"-1-"];
        NSLog(@">>> 1 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"- 2 -"];
        NSLog(@">>> 2 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"- 3 -"];
        NSLog(@">>> 3 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"- 4 -"];
        NSLog(@">>> 4 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [conditionString appendString:@"- 5 -"];
        NSLog(@">>> 5 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
    });
}
Copy the code

Console output:

2019-11-11 20:34:16.875479+0800 DiscoverLock_iOS[93895:11551560] >>> 1--2-- 3 --3 -- threadInfo:<NSThread: 0x600003905640>{number = 4, Name = (NULL)}<<< 2019-11-11 20:34:16.875525+0800 DiscoverLock_iOS[93895:11551562] >>> 3-1 --2--4--3- threadInfo:<NSThread: 0x600003903680>{number = 6, Name = (null)}<<< 2019-11-11 20:34:16.875530+0800 DiscoverLock_iOS[93895:11551561] >>> 1-1 -- 2-threadinfo :<NSThread: 0x600003908bc0>{number = 3, Name = (NULL)}<<< 2019-11-11 20:34:16.875543+0800 DiscoverLock_iOS[93895:11551559] >>> 4-1 --2--4--3- threadInfo:<NSThread: 0x6000039175c0>{number = 5, Name = (NULL)}<<< 2019-11-11 20:34:16.875628+0800 DiscoverLock_iOS[93895:11551560] >>> 5-1 --2--4--3--5- threadInfo:<NSThread: 0x600003905640>{number = 4, name = (null)}<<<Copy the code

The NSLock part of the above has controlled the stability of read and write by locking, so if we want to execute according to the label, what should we do?

“Dispatch_barrier” only applies to a single concurrent queue. Dispatch_queue_t thread = dispatch_queue_create(“barrier”, DISPATCH_QUEUE_CONCURRENT); “Dispatch_get_global_queue (0,0)” is the same as “dispatch_async”

What if we want to do order in different threads? NSConditionLock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock conditionlock

- (void)conditionLockOrdered {
    // NSConditionLock
    NSInteger conditionTag = 0;
    _conditionLock = [[NSConditionLock alloc] initWithCondition:conditionTag];
    
    NSMutableString *conditionString = [[NSMutableString alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@">>> handle 1 <<<");
        [_conditionLock lockWhenCondition:conditionTag];
        [conditionString appendString:@"-1-"];
        NSLog(@">>> 1 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@">>> handle 2 <<<");
        [_conditionLock lockWhenCondition:1];
        [conditionString appendString:@"- 2 -"];
        NSLog(@">>> 2 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:2];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@">>> handle 3 <<<");
        [_conditionLock lockWhenCondition:2];
        [conditionString appendString:@"- 3 -"];
        NSLog(@">>> 3 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:3];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@">>> handle 4 <<<");
        [_conditionLock lockWhenCondition:3];
        [conditionString appendString:@"- 4 -"];
        NSLog(@">>> 4 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock unlockWithCondition:4];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@">>> handle 5 <<<");
        [_conditionLock lockWhenCondition:4];
        [conditionString appendString:@"- 5 -"];
        NSLog(@">>> 5 %@ threadInfo:%@<<<",conditionString,[NSThread currentThread]);
        [_conditionLock  unlock];
        NSLog(@"- = - = - = - = - = - = -");
    });
    NSLog(@"🍺");
}
Copy the code

Console output:

2019-11-11 20:53:58.237847+0800 DiscoverLock_iOS[94374:11586439] 🍺 2019-11-11 20:53:58.237862+0800 DiscoverLock_iOS[94374:11586488] >>> Handle 1 <<< 2019-11-11 20:53:58.237877+0800 DiscoverLock_iOS[94374:11586489] >>> Handle 3 <<< 2019-11-11 20:53:58.237868+0800 DiscoverLock_iOS[94374:11586490] >>> Handle 2 <<< 2019-11-11 20:53:58.237887+0800 DiscoverLock_iOS[94374:11586491] >>> Handle 4 <<< 2019-11-11 20:53:58.237892+0800 DiscoverLock_iOS[94374:11586495] >>> Handle 5 <<< 2019-11-11 20:53:58.238111+0800 DiscoverLock_iOS[94374:11586488] >>> 1  -1- threadInfo:<NSThread: 0x6000014c3380>{number = 3, Name = (null)}<<< 2019-11-11 20:53:58.238488+0800 DiscoverLock_iOS[94374:11586490] >>> 2-1 -- 2-threadinfo :<NSThread: 0x6000014dac40>{number = 4, Name = (null)}<<< 2019-11-11 20:53:58.238605+0800 DiscoverLock_iOS[94374:11586489] >>> 3-1 --2-- 3-threadInfo :<NSThread:  0x6000014daf00>{number = 5, Name = (NULL)}<<< 2019-11-11 20:53:58.239269+0800 DiscoverLock_iOS[94374:11586491] >>> 4-1 --2--3--4- threadInfo:<NSThread: 0x6000014c6740>{number = 6, Name = (NULL)}<<< 2019-11-11 20:53:58.239410+0800 DiscoverLock_iOS[94374:11586495] >>> 5-1 --2--3--4--5- threadInfo:<NSThread: 0x6000014c3480>{number = 7, Name = (null)} < < < 2019-11-11 20:53:58. 239552 + 0800 DiscoverLock_iOS [94374-11586495] - = - = - = - = - = - = -Copy the code

It can be seen that although different threads are scheduled at different times, due to the existence of NSConditionLock, the sequence we expected for the subsequent specific operations on data is guaranteed. I don’t see any obvious problems with this usage in cases where tasks are less time-consuming, but I recommend using dispatch_barrier if there are longer time-consuming operations because it doesn’t take up too much resources

NSRecursiveLock

  • - (BOOL)tryLock;: Attempts to lock, returns NO on failure, does not block the thread
  • - (BOOL)lockBeforeDate:(NSDate *)limit;: Tries to lock before the specified time, if failed to return NO, to the time before the blocking thread Api sameNSLockExactly the same. The difference isNSRecursiveLockThe lock can be repeatedly added in the same thread without being unlocked. It will record the number of times the lock is locked and the number of times the lock is unlocked. When the two values are balanced, the lock will be released and other threads can be locked successfully

Let’s look at the next piece of code and see what the problem is:

@property (nonatomic, assign) NSInteger recursiveNum; // 5 - (void)test_unrecursiveLock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self recursiveTest]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ self.recursiveNum = 7; NSLog(@">>> changed %ld <<<",self.recursiveNum);
    });
}

- (void)recursiveTest {
    if (self.recursiveNum > 0) {
        self.recursiveNum -= 1;
        NSLog(@">>> %ld <<<",self.recursiveNum); [self recursiveTest]; }}Copy the code

Console output:

2019-11-11 21:27:13.451703+0800 DiscoverLock_iOS[95105:11645279] >>> 4 <<< < 2019-11-11 21:27:13.451709+0800 Changed 7 <<< 2019-11-11 21:27:13.451812+0800 DiscoverLock_iOS[95105:11645279] >>> 6 <<< 2019-11-11 21:27:13.451883+0800 DiscoverLock_iOS[95105:11645279] >>> 5 <<< 2019-11-11 21:27:13.451940+0800 DiscoverLock_iOS[95105:11645279] >>> 4 <<< DiscoverLock_iOS[95105:11645279] >>> 3 <<< 2019-11-11 21:27:13.452068+0800 DiscoverLock_iOS[95105:11645279] >>> 2 <<< < 2019-11-11 21:27:13.452130+0800 DiscoverLock_iOS[95105:11645279] >>> 1 <<< DiscoverLock_iOS[95105:11645279] >>> 0 <<<Copy the code

There are two threads writing to a known recursiveNum value. One thread performs a recursive call on the value, but the other thread changes it at the same time. Without locking, this operation is problematic. A race can result in the entire logic being executed with a high probability of being incorrect.

To avoid unnecessary errors caused by this race, the first thing to think about is locking, but if you recursively lock, the thread will repeat locking, resulting in deadlocks. So you have to use recursive locking to solve this problem

- (void)test_unrecursiveLock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self recursiveTest]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [_recursiveLock lock]; Self. RecursiveNum = 7; NSLog(@">>> changed %ld <<<",self.recursiveNum); [_recursiveLock unlock]; / / unlock}); } - (void)recursiveTest { [_recursiveLock lock]; / / recursive lockingif (self.recursiveNum > 0) {
        self.recursiveNum -= 1;
        NSLog(@">>> %ld <<<",self.recursiveNum); [self recursiveTest]; } [_recursiveLock unlock]; / / unlock}Copy the code

Console output:

2019-11-11 21:34:44.422337+0800 DiscoverLock_iOS[95341:11655990] >>> 4 <<< < 2019-11-11 21:34:44.422442+0800 DiscoverLock_iOS[95341:11655990] >>> 3 <<< 2019-11-11 21:34:44.422511+0800 DiscoverLock_iOS[95341:11655990] >>> 2 <<< < 2019-11-11 21:34:44.422583+0800 DiscoverLock_iOS[95341:11655990] >>> 1 <<< 2019-11-11 21:34:44.422645+0800 DiscoverLock_iOS[95341:11655990] >>> 0 <<< 2019-11-11 21:34:44.422747+0800 DiscoverLock_iOS[95341:11655992] >>> changed 7 <<< ------ 2019-11-11 21:37:11.238448+0800 DiscoverLock_iOS[95396:11662426] >>> changed 7 <<< 2019-11-11 21:37:11.238635+0800 DiscoverLock_iOS[95396:11662423] >>> 6 <<< 2019-11-11 21:37:11.238793+0800 DiscoverLock_iOS[95396:11662423] >>> 5 <<< DiscoverLock_iOS[95396:11662423] >>> 4 <<< 2019-11-11 21:37:11.239093+0800 DiscoverLock_iOS[95396:11662423] >>> 3 <<< < 2019-11-11 21:37:11.239293+0800 DiscoverLock_iOS[95396:11662423] >>> 2 <<< DiscoverLock_iOS[95396:11662423] >>> 1 <<< 2019-11-11 DiscoverLock_iOS[95396:11662423] >>> 0 <<<Copy the code

There are two outputs, but the logic of our recursive operation is completely undisturbed, and if we need to control the order, (knock on the blackboard) how do we do that?

NSCondition

  • - (void)wait;: The current thread enters the sleep state immediately
  • - (BOOL)waitUntilDate:(NSDate *)limit;: The current thread will sleep immediately and wake up after the limit time
  • - (void)signal;: A single thread that goes to sleep after waking up wait
  • - (void)broadcast;: Wakes up wait for all threads to sleep

There are cases where you need to coordinate execution between threads. For example, one thread might need to wait for another thread to return a result, and NSCondition might be a good choice

To illustrate the role of NSCondition, let’s use a producer-consumer example that may not be very appropriate: At present, we have a flexible production line, which can only produce 3 items per batch, and it takes 6s. Meanwhile, we open the online purchasing platform to let everyone buy a group, order sales, and form a group of three people. Now, three chosen children, Tom/Mike/Lily, stand out from thousands of people around the world and successfully form a group. In order to enhance the playability, the activity starts from the opening moment, the production and the purchase at the same time, after the three pieces of inventory sales are completed, the production and the purchase at the same time again

A code example is as follows:

@interface Producer : NSObject
@property (nonatomic, assign) BOOL shouldProduce;
@property (nonatomic, strong) NSString *itemName;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *collector;

- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector;
- (void)produce;
@end

@implementation Producer

- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector{
    
    self = [super init];
    if (self) {
        self.condition = condition;
        self.collector = collector;
        self.shouldProduce = NO;
        self.itemName = nil;
    }
    return self;
}

-(void)produce{
    self.shouldProduce = YES;
    while (self.shouldProduce) {
        NSLog(@"Ready for production");
        [self.condition lock];
        NSLog(@"- p lock -");
        if (self.collector.count > 0 ) {
            NSLog(@"- p - wait");
            [self.condition wait];
        }
        NSLog(@"Start production");
        [self.collector addObject:@Products "1"];
        [self.collector addObject:@"Item 2"];
        [self.collector addObject:@"Three goods"];
        NSLog(@"Production: Good 1/ Good 2/ Good 3");
        sleep(6);
        NSLog(@"End of production");
        [self.condition broadcast];
        NSLog(@"- p signal -");
        [self.condition unlock];
        NSLog(@"- p unlock -");
    }
    NSLog(@- End of production -);
}

@end

@interface Consumer : NSObject
@property (nonatomic, assign) BOOL shouldConsumer;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *collector;
@property (nonatomic,   copy) NSString *itemName;
- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector name:(NSString *)name;
- (void)consumer;
@end

@implementation Consumer
- (instancetype)initWithConditon:(NSCondition *)condition collector:(NSMutableArray *)collector name:(NSString *)name{
    self = [super init];
    if (self) {
        self.condition = condition;
        self.collector = collector;
        self.shouldConsumer = NO;
        self.itemName = name;
    }
    return self;
}

-(void)consumer{
    self.shouldConsumer = YES;
    while (self.shouldConsumer) {
        NSLog(@"%@- Ready to buy",self.itemName);
        [self.condition lock];
        NSLog(@"- c:%@ lock -",self.itemName);
        if (self.collector.count == 0 ) {
            NSLog(@"- c:%@ wait -",self.itemName);
            [self.condition wait];
        }
        NSString *item = [self.collector objectAtIndex:0];
        NSLog(@"% @ - buying: % @",self.itemName,item);
        [self.collector removeObjectAtIndex:0];
        sleep(2);
        [self.condition signal];
        NSLog(@"- c:%@ signal -",self.itemName);
        [self.condition unlock];
        NSLog(@"- c:%@ unlock -",self.itemName);
    }
    NSLog(@"-%@ End purchase -",self.itemName); } @end // call {NSMutableArray *pipeline = [NSMutableArray array]; NSCondition *condition = [NSCondition new]; Producer *p = [[Producer alloc] initWithConditon:condition collector:pipeline]; Consumer *c = [[Consumer alloc] initWithConditon:condition collector:pipeline name:@"Tom"];
    Consumer *c1 = [[Consumer alloc] initWithConditon:condition collector:pipeline name:@"Mike"];
    Consumer *c2 = [[Consumer alloc] initWithConditon:condition collector:pipeline name:@"Lily"];
    [[[NSThread alloc] initWithTarget:c selector:@selector(consumer) object:c] start];
    [[[NSThread alloc] initWithTarget:c1 selector:@selector(consumer) object:c] start];
    [[[NSThread alloc] initWithTarget:c2 selector:@selector(consumer) object:c] start];
    [[[NSThread alloc] initWithTarget:p selector:@selector(produce) object:p] start];
    

    sleep(15);
    NSLog(@"< -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- >");
    p.shouldProduce = NO;
    c.shouldConsumer = NO;
    c1.shouldConsumer = NO;
    c2.shouldConsumer = NO;
}
Copy the code

Partial console output:

2019-11-12 17:04:03.662926+0800 DiscoverLock_iOS[7110:12246052] 2019-11-12 17:04:03.662916+0800 DiscoverLock_iOS[7110:12246051] Tom- Ready to buy 2019-11-12 17:04:03.662990+0800 DiscoverLock_iOS[7110:12246053] Lily- Ready to buy 2019-11-12 17:04:03.663005+0800 DiscoverLock_iOS[7110:12246054] 2019-11-12 17:04:03.663083+0800 DiscoverLock_iOS[7110:12246053] -c :Lily lock - 2019-11-12 17:04:03.663144+0800 DiscoverLock_iOS[7110:12246053] -c :Lilywait2019-11-12 17:04:03.663254+0800 DiscoverLock_iOS[7110:12246052] -c :Mike lock - 2019-11-12 17:04:03.663439+0800 DiscoverLock_iOS[7110:12246052] - c:Mikewait-2019-11-12 17:04:03.663805+0800 DiscoverLock_iOS[7110:12246051] -c: 2019-11-12 17:04:03.663903+0800 DiscoverLock_iOS[7110:12246051] - c:Tomwait-2019-11-12 17:04:03.664126+0800 DiscoverLock_iOS[7110:12246054] -p lock - 2019-11-12 17:04:03.664297+0800 DiscoverLock_iOS[7110:12246054] Start Production 2019-11-12 17:04:03.664433+0800 DiscoverLock_iOS[7110:12246054] Produce: Commodity 1/ Commodity 2/ Commodity 3 2019-11-12 17:04:09.669735+0800 DiscoverLock_iOS[7110:12246054] Production is completeCopy the code

Based on the working principle of multi-threaded concurrency, it is also easy to reach this conclusion by printing the results in part above. Since Lily/Mike/Tom cannot meet the purchase conditions, they can only choose wait. At this time, the producer acquires the lock and executes the production code. After the production is complete, broadcast or signal tells other threads that they can wake up and continue executing the consumer-related code. The difference between NSCondition and NSConditionLock is that it relies on external values and can meet more complex demand scenarios. If you replace the broadcast of the producer with signal in the above code, there seems to be no difference between the two methods in this particular scenario. And if you are interested, you can run the code several times and see if you can come up with the same guess as the author:

  1. Nsconditions themselves coordinate the scheduling of tasks through queue management
  2. Wait tasks are queued in sequence
  3. Unwaited tasks are queued in the order in which they acquire locks
  4. Wait queues of WAIT tasks are merged into execution queues after the execution queues are complete
  5. The first dispatch order is determined, the execution of follow-up tasks, in accordance with the execution queue cache in turn out of the execution here only do guess, specific implementation may not be so, to the tanagers or have the opportunity to study

OSSpinLock

OSSpinLock is a spin-lock solution provided by apple before iOS10, but there is a problem with priority inversion, which was discarded by apple. OSSpinLock was used in the source code before the place. All replaced by Apple with pthread_mutex

os_unfair_lock

Os_unfair_lock os_unfair_lock is a low-level locking solution for iOS10. It is a mutex lock

  • void os_unfair_lock_lock(os_unfair_lock_t lock);: lock
  • bool os_unfair_lock_trylock(os_unfair_lock_t lock);: Attempts to lock, returns true on success, false on failure
  • void os_unfair_lock_unlock(os_unfair_lock_t lock);: unlock
  • void os_unfair_lock_assert_owner(os_unfair_lock_t lock);: Raises assertions if the current thread does not hold the specified lock
  • void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);: Raises assertions if the current thread holds the specified lock

Each method is not much different from the common lock, you can see the method annotation, just need to pay attention to the initialization method

{
    os_unfair_lock_t unfairLock;
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
}
Copy the code

@synchronize(object)

The @synchronized(object) instruction uses the passed object as the unique identifier of the lock. Only when the identifier is the same, it can meet the mutual exclusion. The advantage of the @synchronized(object) instruction to realize the lock is that we do not need to explicitly create the lock object in the code, and then we can realize the locking mechanism. And don’t worry about forgetting that unlocking is extremely common, so I won’t do the example

dispatch_semaphore

  • dispatch_semaphore_t dispatch_semaphore_create(long value);: Creates a semaphore, passing in an initial value
  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);: When the signal <=0, the thread blocks according to the time passed in; If signal >0, the thread is not blocked and signal -1 is processed
  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema);: Signal +1 processing

The semaphores provided by GCD are also commonly used for locking. The common use is to initialize the signal value to 1

{ dispatch_semaphore_t lock = dispatch_semaphore_create(1); dispatch_semaphore_wait(lock,DISPATCH_TIME_FOREVER); / / operation dispatch_semaphare_signal (lock); }Copy the code

Routine operations As we all know, there must be routine operations, then there must be unconventional operations, you can take a look at the AFNetwork to give us a demonstration

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}
Copy the code

In AFURLSessionManager, initialize with dispatch_semaphoRE_create (0) and return tasks; Call dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); Block the thread and execute dispatch_semaphore_signal(semaphore) after the block assigns the target value. At this point, tasks has a value and the thread returns normally after being woken up. Very beautiful

pthread_mutex

The mutex scheme in C is the bottom layer of the four classes in the protocol

Lock common functions:

  • pthread_mutex_init: Initializes the mutex dynamically
  • PTHREAD_MUTEX_INITIALIZER: Statically creates a mutex
  • pthread_mutex_lock: Locks a mutex
  • pthread_mutex_trylock: Locks, does not block if it fails
  • pthread_mutex_unlock: unlock
  • pthread_mutex_destroy: destroy the lock

Parameter configuration functions:

  • pthread_mutexattr_init: Initialization parameter
  • pthread_mutexattr_settype: Setting type
  • pthread_mutexattr_setpshared: Sets the scope
  • pthread_mutexattr_destroy: Destroy parameters

Conditional common functions:

  • pthread_cond_init: Dynamic initialization condition quantity
  • PTHREAD_COND_INITIALIZER: Statically creates a conditional
  • pthread_cond_wait: Pass in the conditional quantity and lock
  • pthread_cond_signal: Wakes up a single thread and locks it
  • pthread_cond_broadcast: broadcast wakes up all threads
  • pthread_cond_destroy: Destruction conditions

All of the above functions return a value. Note that if the function is successful, it returns 0. Otherwise, it returns an error number

The lock type:

  • PTHREAD_MUTEX_NORMAL: Default value. This type of mutex does not automatically detect deadlocks. If a thread attempts to lock a mutex repeatedly, the thread will be deadlocked. Trying to unlock a mutex locked by another thread can cause unexpected results. A thread attempting to unlock a mutex that has already been unlocked can also cause unexpected results
  • PTHREAD_MUTEX_ERRORCHECK: This type of mutex automatically detects deadlocks. If a thread attempts to lock a mutex repeatedly, an error code is returned. Attempts to unlock a mutex locked by another thread will return an error code. An error code is also returned if a thread attempts to unlock a mutex that has already been unlocked
  • PTHREAD_MUTEX_RECURSIVE: If a thread repeatedly locks a mutex of this type, it does not cause a deadlock. If a thread repeatedly locks a mutex of this type, the same number of locks must be repeated by the thread in order to unlock the mutex, and other threads can acquire the mutex. Attempts to unlock a mutex locked by another thread will return an error code. An error code is also returned if a thread attempts to unlock a mutex that has already been unlocked. This type of mutex can only be process-private (the scoped attribute PTHREAD_PROCESS_PRIVATE)
  • PTHREAD_MUTEX_DEFAULT: Indicates the NORMAL type

Lock scope:

  • PTHREAD_PROCESS_PRIVATE: The default value. The scope is intra-process
  • PTHREAD_PROCESS_SHARED: scope is interprocess

Example:

static pthread_mutex_t c_lock;
- (void)testPthread_mutex {
    pthread_mutexattr_t c_lockAttr;
    pthread_mutexattr_init(&c_lockAttr);
    pthread_mutexattr_settype(&c_lockAttr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutexattr_setpshared(&c_lockAttr, PTHREAD_PROCESS_PRIVATE);
    
    pthread_mutex_init(&c_lock, &c_lockAttr);
    pthread_mutexattr_destroy(&c_lockAttr);

    pthread_t thread1;
    pthread_create(&thread1, NULL, _thread1, NULL);
    
    pthread_t thread2;
    pthread_create(&thread2, NULL, _thread2, NULL);
}

void *_thread1() {
    pthread_mutex_lock(&c_lock);
    printf("thread 1\n");
    pthread_mutex_unlock(&c_lock);
    return 0;
}

void *_thread2() {
    pthread_mutex_lock(&c_lock);
    printf("thread 2 busy\n");
    sleep(3);
    printf("thread 2\n");
    pthread_mutex_unlock(&c_lock);
    return 0;
}
Copy the code

Caution in using locks

  1. Mutex takes time to lock and unlock. Programs that lock fewer mutex will generally run faster. Therefore, the mutex should be limited to enough, and the area protected by each mutex should be as large as possible.

  2. Mutexes by their very nature are executed serially. If many threads have to manically lock the same mutex, the thread will spend most of its time waiting, which can be detrimental to performance. If the data (or code) protected by a mutex contains fragments that are unrelated to each other, a very large mutex can be broken up into several smaller mutex to improve performance. As a result, there are fewer threads requiring a small mutex at any one time, and the thread wait time is reduced. Therefore, the mutex should be large enough (to be meaningful) that each mutex should protect as little area as possible.

Reference documentation

Posix mutex pthread_mutex_t OSSpinLock How does @synchronized Lock /unlock in Objective-C? What is the difference between atomic and nonatomic? High Performance iOS App Development Chinese Version