iOS中的锁

2017-05-09  本文已影响10人  c048e8b8e3d7

引子

一个经典的卖票场景(三个窗口同时卖10张票)可以描述多线程操作同一数据资源出现的问题。

@property(nonatomic, assign) NSInteger tickets;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.tickets = 10;

    NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    threadA.name = @"A";
    
    NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    threadB.name = @"B";
    
    NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    threadC.name = @"C";
    
    [threadA start];
    [threadB start];
    [threadC start];
}

- (void)saleTickets
{
    NSThread *thread = [NSThread currentThread];
    
    while (1) {
        if (_tickets > 0) {
            [NSThread sleepForTimeInterval:1];
            _tickets--;
            NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@",[NSThread currentThread]);
            break;
        }
    }
}

输出结果可以发现数据出现了错乱

C 卖出一张票 剩余票数= 8, Thread:<NSThread: 0x600000076dc0>{number = 7, name = C}
A 卖出一张票 剩余票数= 9, Thread:<NSThread: 0x600000064a80>{number = 5, name = A}
B 卖出一张票 剩余票数= 7, Thread:<NSThread: 0x600000073580>{number = 6, name = B}
B 卖出一张票 剩余票数= 4, Thread:<NSThread: 0x600000073580>{number = 6, name = B}
C 卖出一张票 剩余票数= 4, Thread:<NSThread: 0x600000076dc0>{number = 7, name = C}
A 卖出一张票 剩余票数= 4, Thread:<NSThread: 0x600000064a80>{number = 5, name = A}
A 卖出一张票 剩余票数= 1, Thread:<NSThread: 0x600000064a80>{number = 5, name = A}
B 卖出一张票 剩余票数= 3, Thread:<NSThread: 0x600000073580>{number = 6, name = B}
C 卖出一张票 剩余票数= 1, Thread:<NSThread: 0x600000076dc0>{number = 7, name = C}
C 卖出一张票 剩余票数= -2, Thread:<NSThread: 0x600000076dc0>{number = 7, name = C}
A 卖出一张票 剩余票数= -2, Thread:<NSThread: 0x600000064a80>{number = 5, name = A}
B 卖出一张票 剩余票数= -2, Thread:<NSThread: 0x600000073580>{number = 6, name = B}
票卖完了  Thread:<NSThread: 0x600000076dc0>{number = 7, name = C}
票卖完了  Thread:<NSThread: 0x600000064a80>{number = 5, name = A}
票卖完了  Thread:<NSThread: 0x600000073580>{number = 6, name = B}

锁的总类及其效率

iOS开发中常用的锁有如下几种:

1 @synchronized
2 NSLock 对象锁
3 NSRecursiveLock 递归锁
4 NSConditionLock 条件锁
5 pthread_mutex 互斥锁(C语言)
6 dispatch_semaphore 信号量实现加锁(GCD)
7 OSSpinLock (暂不建议使用,原因见参考3)
性能对比

synchronized(关键字加锁 互斥锁,性能较差不推荐使用)

使用方法

@synchronized(这里添加一个OC对象,一般使用self) {
       //这里写要加锁的代
}
注意点
1.加锁的代码尽量少
2.添加的OC对象必须在多个线程中都是同一对象
3.优点是不需要显式的创建锁对象,便可以实现锁的机制。
4.@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

示例

- (void)saleTicketsBySynchronized
{
    NSThread *thread = [NSThread currentThread];
    
    while (1) {
        @synchronized (self) {
            
            if (_tickets > 0) {
                [NSThread sleepForTimeInterval:1];
                _tickets--;
                NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
            } else {
                NSLog(@"票卖完了  Thread:%@",[NSThread currentThread]);
                break;
            }
        }
    }
}

NSLock(互斥锁)

注意点
1 锁定(lock)和解锁(unLock)必须配对使用
2 同一锁对象才会互斥(如果把lock的初始化放在saleTicketsByNSLock方法中,则不起作用)

NSLock类还提供tryLock方法,意思是尝试锁定,当锁定失败时,不会阻塞进程,而是会返回NO。你也可以使用lockBeforeDate:方法,意思是在指定时间之前尝试锁定,如果在指定时间前都不能锁定,也是会返回NO。

@property(nonatomic, strong) NSLock *lock;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = [NSLock new];
}

- (void)saleTicketsByNSLock
{
    NSThread *thread = [NSThread currentThread];
    
    while (1) {
        
        [self.lock lock];
        if (_tickets > 0) {
            [NSThread sleepForTimeInterval:1];
            _tickets--;
            NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@",[NSThread currentThread]);
            break;
        }
        [self.lock unlock];
    }
}

NSRecursiveLock 递归锁

递归锁也可以像NSLock一样使用,只是对象名称换了,这里就不列举卖票的示例了

使用锁最容易犯的一个错误就是在递归或循环中造成死锁
如下代码中,在递归方法中,锁会被多次的lock,所以自己也被阻塞了

@property(nonatomic, strong) NSLock *lock;
@property(nonatomic, strong) NSRecursiveLock *recursiveLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.lock = [NSLock new];
    self.recursiveLock = [NSRecursiveLock new];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self testMethod:5];
    });
}

- (void)testMethod:(NSInteger)num
{
    [_lock lock];
    if (num > 0) {
        [NSThread sleepForTimeInterval:1];
        NSLog(@"number = %ld -- %@", num--, [NSThread currentThread]);
        
        [self testMethod:num];
    }
    
    [_lock unlock];
}

number = 5 -- <NSThread: 0x608000261200>{number = 3, name = (null)}
*** -[NSLock lock]: deadlock (<NSLock: 0x6080000dbd60> '(null)')
*** Break on _NSLockError() to debug.

testMethod方法中将NSLock对象换成NSRecursiveLock对象便可解决问题。
NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。
递归锁会跟踪它被多少次lock,每次成功的lock都必须平衡调用unlock操作。
只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得

- (void)testMethod:(NSInteger)num
{
    [_recursiveLock lock];
    if (num > 0) {
        [NSThread sleepForTimeInterval:1];
        NSLog(@"number = %ld -- %@", num--, [NSThread currentThread]);
        
        [self testMethod:num];
    }
    
    [_recursiveLock unlock];
}

NSConditionLock(条件锁)

使用NSConditionLock的lock和unlock方法并不能很好地解决卖票的问题,还在研究中...

pthread_mutex(互斥锁)

需要#import <pthread.h>

核心代码

__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);

示例

- (void)viewDidLoad {
    [super viewDidLoad];

    __block NSInteger count = 10;
    
    __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        while (1) {
            pthread_mutex_lock(&mutex);
            if (count > 0) {
                NSLog(@"A count = %ld", (long)(count--));
                sleep(1);
            } else {
                pthread_mutex_unlock(&mutex);
                break;
            }
            pthread_mutex_unlock(&mutex);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        while (1) {
            pthread_mutex_lock(&mutex);
            if (count > 0) {
                NSLog(@"B count = %ld", (long)(count--));
                sleep(1);
            } else {
                pthread_mutex_unlock(&mutex);
                break;
            }
            pthread_mutex_unlock(&mutex);
        }
    });
}

dispatch_semaphore(信号量实现加锁)

核心代码

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(semaphore);

示例

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSInteger count = 10;
    
    //创建信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        while (1) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            if (count > 0) {
                NSLog(@"A count = %ld", (long)(count--));
                sleep(1);
            } else {
                break;
            }
            dispatch_semaphore_signal(semaphore);
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        while (1) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            if (count > 0) {
                NSLog(@"B count = %ld", (long)(count--));
                sleep(1);
            } else {
                break;
            }
            dispatch_semaphore_signal(semaphore);
        }
    });
}

OSSpinLock(不建议使用)

#import <libkern/OSAtomic.h>

@property(nonatomic, assign) OSSpinLock pinLock;

- (void)viewDidLoad {
    [super viewDidLoad];

    _pinLock = OS_SPINLOCK_INIT;
}

- (void)saleTickets
{
    NSThread *thread = [NSThread currentThread];
    
    while (1) {
        
        OSSpinLockLock(&_pinLock);
        
        if (_tickets > 0) {
            [NSThread sleepForTimeInterval:1];
            _tickets--;
            NSLog(@"%@ 卖出一张票 剩余票数= %ld, Thread:%@", thread.name, _tickets,[NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@",[NSThread currentThread]);
            break;
        }
        
        OSSpinLockUnlock(&_pinLock);
        
    }
}

测试发现,票全部是被某一个线程全部卖掉的,不符合实际情况

参考链接

参考1
参考2
参考3

上一篇下一篇

猜你喜欢

热点阅读