级别: ★★☆☆☆
标签:「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);

<span class="hljs-keyword">for</span> (NSInteger i=0; i&lt;10; i++) {
    dispatch_async(queue, ^{
        [self <span class="hljs-built_in">test</span>NSLock];
    });
}

}

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

// 测试方法
- (void)testNSLock {

<span class="hljs-keyword">while</span> (1) {
    [_lock lock];
    <span class="hljs-keyword">if</span> (_ticketCount &gt; 0) {
        _ticketCount --;
        NSLog(@<span class="hljs-string">"---&gt;&gt; %@已购票1张,剩余%ld张"</span>, [NSThread currentThread], (long)_ticketCount);
    }
    <span class="hljs-keyword">else</span> {
        [_lock unlock];
        <span class="hljs-built_in">return</span>;
    }
    [_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(<span class="hljs-string">"QiMultiThreadSafeQueue"</span>, DISPATCH_QUEUE_CONCURRENT);
<span class="hljs-keyword">for</span> (NSInteger i=0; i&lt;2; i++) {
    dispatch_async(queue, ^{
        [self <span class="hljs-built_in">test</span>DispatchSemaphore:i];
    });
}

}

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

// 测试方法
- (void)testDispatchSemaphore:(NSInteger)num {

<span class="hljs-keyword">while</span> (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)));
    <span class="hljs-keyword">if</span> (ret == 0) {
        <span class="hljs-keyword">if</span> (_ticketCount &gt; 0) {
            NSLog(@<span class="hljs-string">"%d 窗口 卖了第%d张票"</span>, (int)num, (int)_ticketCount);
            _ticketCount --;
        }
        <span class="hljs-keyword">else</span> {
            dispatch_semaphore_signal(_semaphore);
            NSLog(@<span class="hljs-string">"%d 卖光了"</span>, (int)num);
            <span class="hljs-built_in">break</span>;
        }
        [NSThread sleepForTimeInterval:0.2];
        dispatch_semaphore_signal(_semaphore);
    }
    <span class="hljs-keyword">else</span> {
        NSLog(@<span class="hljs-string">"%d %@"</span>, (int)num, @<span class="hljs-string">"超时了"</span>);
    }
    
    [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(<span class="hljs-string">"QiMultiThreadSafeQueue"</span>, DISPATCH_QUEUE_CONCURRENT);

<span class="hljs-keyword">for</span> (NSInteger i=0; i&lt;10; i++) {
    dispatch_async(queue, ^{
        [self <span class="hljs-built_in">test</span>NSConditionAdd];
    });
}

<span class="hljs-keyword">for</span> (NSInteger i=0; i&lt;10; i++) {
    dispatch_async(queue, ^{
        [self <span class="hljs-built_in">test</span>NSConditionRemove];
    });
}

}

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

// 测试方法
- (void)testNSConditionAdd {

[_condition lock];

// 生产数据
NSObject *object = [NSObject new];
[_ticketsArr addObject:object];
NSLog(@<span class="hljs-string">"---&gt;&gt;%@ add"</span>, [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(<span class="hljs-string">"QiMultiThreadSafeQueue"</span>, DISPATCH_QUEUE_CONCURRENT);

<span class="hljs-keyword">for</span> (NSInteger i=0; i&lt;10; i++) {
    dispatch_async(queue, ^{
        [self <span class="hljs-built_in">test</span>NSConditionLockAdd];
    });
}

<span class="hljs-keyword">for</span> (NSInteger i=0; i&lt;10; i++) {
    dispatch_async(queue, ^{
        [self <span class="hljs-built_in">test</span>NSConditionLockRemove];
    });
}

}

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

// 测试方法
- (void)testNSConditionLockAdd {

// 满足CONDITION_NO_DATA时,加锁
[_conditionLock lockWhenCondition:CONDITION_NO_DATA];

// 生产数据
NSObject *object = [NSObject new];
[_ticketsArr addObject:object];
NSLog(@<span class="hljs-string">"----&gt;&gt;%@ add"</span>, [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];

<span class="hljs-keyword">if</span> (tag &gt; 0) {
    
    [self <span class="hljs-built_in">test</span>NSRecursiveLock:tag - 1];
    NSLog(@<span class="hljs-string">"---&gt;&gt; %ld"</span>, (long)tag);
}

[_recursiveLock unlock];

}

复制代码
6. @synchronized

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

- (void)testSynchronized {
@synchronized (self) {
    
    <span class="hljs-keyword">if</span> (_ticketCount &gt; 0) {
        
        _ticketCount --;
        NSLog(@<span class="hljs-string">"---&gt;&gt; %@已购票1张,剩余%ld张"</span>, [NSThread currentThread], (long)_ticketCount);
    }
}

}

复制代码

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

本文参考了以下文章:

工程源码 GitHub 地址


小编微信:可加并拉入《QiShare 技术交流群》。

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

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

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

  • IOS

    iOS 是由蘋果公司為 iPhone 開發的操作系統。它主要是給 iPhone、iPod touch、iPad 以及 Apple TV 使用。就像其基於的 Mac OS X 操作系統一樣,它也是以 Darwin 為基礎的…

    189 引用 • 1 回帖
感谢    赞同    分享    收藏    关注    反对    举报    ...