美文网首页
iOS中的锁

iOS中的锁

作者: c048e8b8e3d7 | 来源:发表于2017-05-09 22:08 被阅读10次

    引子

    一个经典的卖票场景(三个窗口同时卖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

    相关文章

      网友评论

          本文标题:iOS中的锁

          本文链接:https://www.haomeiwen.com/subject/cqqitxtx.html