iOS 多线程之线程安全

avatar
奇舞团移动端团队 @奇舞团

级别: ★★☆☆☆
标签:「iOS」「多线程」「线程安全」
作者: dac_1033
审校: QiShare团队


一、线程安全问题

在单线程的情形下,任务依次串行执行是不存在线程安全问题的。在单线程的情形下,如果多线程都是访问共享资源而不去修改共享资源也可以保证线程安全,比如:设置只读属性的全局变量。线程不安全是由于多线程访问造成的,是由于多线程访问和修改共享资源而引起不可预测的结果。而线程锁可以有效的解决线程安全问题,大致过程如下图:

无线程锁

加线程锁

iOS 多线程开发中为保证线程安全而常用的几种锁:NSLockdispatch_semaphoreNSConditionNSRecursiveLockNSConditionLock@synchronized,这几种锁各有优点,适用于不同的场景,下面我们就来依次介绍一下。

二、iOS中的锁

1. NSLock

NSLock 是OC层封装底层线程操作来实现的一种锁,继承NSLocking协议,在此我们不讨论各种锁的实现细节,因为基本用不到。NSLock使用非常简单:

NSLock *lock = [NSLock alloc] init];

// 加锁
[lock lock];

/*
* 被加锁的代码区间
*/

// 解锁
[lock Unlock];

我们以车站购票为例子,多个窗口同时售票,每个窗口有人循环购票:

// 定义NSLock变量
@property (nonatomic, strong) NSLock *lock;
// 实例化
_lock = [[NSLock alloc] init];

/*******************************************************************************/

// 调用测试方法
dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSLock];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testNSLock {
    
    while (1) {
        [_lock lock];
        if (_ticketCount > 0) {
            _ticketCount --;
            NSLog(@"--->> %@已购票1张,剩余%ld张", [NSThread currentThread], (long)_ticketCount);
        }
        else {
            [_lock unlock];
            return;
        }
        [_lock unlock];
        sleep(0.2);
    }
}
2. dispatch_semaphore

dispatch_semaphore 是 GCD 提供的,使用信号量来控制并发线程的数量(可同时进入并执行加锁代码块的线程的数量),相关的三个函数:

// 创建信号量
dispatch_semaphore_create(long value); 

//等待信号
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

发送信号
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
//! 定义信号量semaphore
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
//! 实例化
_semaphore = dispatch_semaphore_create(1);

/*******************************************************************************/

// 调用测试方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    for (NSInteger i=0; i<2; i++) {
        dispatch_async(queue, ^{
            [self testDispatchSemaphore:i];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testDispatchSemaphore:(NSInteger)num {
    
    while (1) {
        // 参数1为信号量;参数2为超时时间;ret为返回值
        //dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        long ret = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.21*NSEC_PER_SEC)));
        if (ret == 0) {
            if (_ticketCount > 0) {
                NSLog(@"%d 窗口 卖了第%d张票", (int)num, (int)_ticketCount);
                _ticketCount --;
            }
            else {
                dispatch_semaphore_signal(_semaphore);
                NSLog(@"%d 卖光了", (int)num);
                break;
            }
            [NSThread sleepForTimeInterval:0.2];
            dispatch_semaphore_signal(_semaphore);
        }
        else {
            NSLog(@"%d %@", (int)num, @"超时了");
        }
        
        [NSThread sleepForTimeInterval:0.2];
    }
}

当第一各参数semaphore取值为1时,dispatch_semaphore_wait(semaphore, timeout)与dispatch_semaphore_signal(signal)成对出现,所达到的效果就跟NSLock中的lock和unlock是一样的。区别在于当semaphore取值为n时,则可以有n个线程同时访问被保护的临界区,即可以控制多个线程并发。第二个参数为dispatch_time_t类型,如果直接输入一个非dispatch_time_t的值会导致dispatch_semaphore_wait方法偶尔返回非0值。

3. NSCondition

NSCondition 常用于生产者-消费者模式,它继承于NSLocking协议,同样有lock和unlock方法。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待数据就绪,再唤醒线程。

NSCondition *lock = [[NSCondition alloc] init];

//线程A
[lock lock];

[lock wait]; // 线程被挂起

[lock unlock];

//线程2
sleep(1);//以保证让线程2的代码后执行

[lock lock];

[lock signal]; // 唤醒线程1

[lock unlock];

我们执行了两次for循环,起了两批新线程,一批来add数据,另一批来remove数据。其中add数据方法加锁,remove数据方法也加了锁:

// 定义变量
@property (nonatomic, strong) NSCondition *condition;
// 实例化
_condition = [[NSCondition alloc] init];

/*******************************************************************************/

// 调用测试方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionAdd];
        });
    }

    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionRemove];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testNSConditionAdd {
    
    [_condition lock];
    
    // 生产数据
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"--->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    [_condition unlock];
}

- (void)testNSConditionRemove {
    
    [_condition lock];
    
    // 消费数据
    if (!_ticketsArr.count) {
        NSLog(@"--->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"--->>%@ remove", [NSThread currentThread]);
    
    [_condition unlock];
}
4. NSConditionLock

NSConditionLock 为条件锁,lockWhenCondition:方法是当condition参数与初始化时候的 condition 相等时才可加锁。而unlockWithCondition:方法并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值。NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:

// 设置条件
#define CONDITION_NO_DATA   100
#define CONDITION_HAS_DATA  101

/*******************************************************************************/

// 初始化条件锁对象
@property (nonatomic, strong) NSConditionLock *conditionLock;
// 实例化
_conditionLock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];

/*******************************************************************************/

// 调用测试方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionLockAdd];
        });
    }

    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionLockRemove];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testNSConditionLockAdd {
    
    // 满足CONDITION_NO_DATA时,加锁
    [_conditionLock lockWhenCondition:CONDITION_NO_DATA];
    
    // 生产数据
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"---->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    // 有数据,解锁并设置条件
    [_conditionLock unlockWithCondition:CONDITION_HAS_DATA];
}

- (void)testNSConditionLockRemove {
    
    // 有数据时,加锁
    [_conditionLock lockWhenCondition:CONDITION_HAS_DATA];
    
    // 消费数据
    if (!_ticketsArr.count) {
        NSLog(@"---->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"---->>%@ remove", [NSThread currentThread]);
    
    //3. 没有数据,解锁并设置条件
    [_conditionLock unlockWithCondition:CONDITION_NO_DATA];
}
5. NSRecursiveLock

顾名思义,NSRecursiveLock定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。NSRecursiveLock在识别到递归时,只加1次锁,在递归返回时也只解锁1次。

// 初始化锁对象
@property (nonatomic, strong) NSRecursiveLock *recursiveLock;
_recursiveLock = [[NSRecursiveLock alloc] init];

/*******************************************************************************/

// 加锁的递归方法
- (void)testNSRecursiveLock:(NSInteger)tag {
    
    [_recursiveLock lock];
    
    if (tag > 0) {
        
        [self testNSRecursiveLock:tag - 1];
        NSLog(@"--->> %ld", (long)tag);
    }
    
    [_recursiveLock unlock];
}
6. @synchronized

@synchronized是一个 OC 层面的锁,非常简单易用。参数需要传一个 OC 对象,它实际上是把这个对象当做锁的唯一标识。使用时直接将加锁的代码区间放入花括号中即可,但是它的缺点也显而易见,虽然易用,但是没有之上介绍几个锁的复杂功能

- (void)testSynchronized {
    
    @synchronized (self) {
        
        if (_ticketCount > 0) {
            
            _ticketCount --;
            NSLog(@"--->> %@已购票1张,剩余%ld张", [NSThread currentThread], (long)_ticketCount);
        }
    }
}

原子操作 原子操作是指不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。如文章开头出图中17+1 = 18这个动作,在整个运算过程中,就属于一个原子操作。
变量属性Property中的原子定义 一般我们定义一个变量 @property (nonatomic, strong) NSMutableArray *ticketsArr; nonatomic:非原子属性,不会为setter方法加锁,适合内存小的移动设备; atomic:原子属性,默认为setter方法加锁(默认就是atomic),线程安全。
PS: 在iOS开发过程中,一般都将属性声明为nonatomic,尽量避免多线程抢夺同一资源,尽量将加锁等资源抢夺业务交给服务器。

本文参考了以下文章:

非常感谢!

工程源码GitHub地址


推荐文章:
iOS 多线程之GCD
iOS 多线程之NSOperation
iOS 多线程之NSThread
iOS Winding Rules 缠绕规则
iOS 签名机制
iOS 扫描二维码/条形码
奇舞周刊

推荐活动:
360粉丝团:136个新年福袋,你确定不来参加吗?