99

iOS开发进阶:线程同步技术-锁

 5 years ago
source link: https://jesuslove.github.io/2018/09/26/iOS开发进阶:线程同步技术-锁/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

NFR32ei.png!web

在iOS多线程中,经常会出现资源竞争和死锁的问题。本节将学习iOS中不同的锁。

线程同步方案

常见的两个问题:多线程买票和存取钱问题。

示例:存取钱问题

// 示例:存取钱问题
- (void)moneyTest {
    self.moneyCount = 100;
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saveMoney];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self takeMoney];
        }
    });
}
- (void)saveMoney {
    int oldCount = self.moneyCount;
    sleep(0.2);
    oldCount += 50;
    self.moneyCount = oldCount;
    NSLog(@"存50,还剩%d钱", self.moneyCount);
}

- (void)takeMoney {
    int oldCount = self.moneyCount;
    sleep(0.2);
    oldCount -= 20;
    self.moneyCount = oldCount;
    NSLog(@"取20,还剩%d钱", self.moneyCount);
}

示例:卖票问题

// 示例:买票
- (void)sellTest {
    self.count = 15;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self printTest2];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self printTest2];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self printTest2];
        }
    });
}
- (void)printTest2 {
    NSInteger oldCount = self.count;
    sleep(0.2);
    oldCount --;
    self.count = oldCount;
    NSLog(@"还剩%ld张票 - %@", (long)oldCount, [NSThread currentThread]);
}

解决上面这种资源共享问题,就需要使用线程同步技术。线程同步技术的核心是:锁。下面学习iOS中不同锁的使用,比较不同锁之间的优缺点。

示例代码: 演示购票和存取钱问题:Demo

iOS当中有哪些锁?

@synchronized 常用于单例
atomic 原子性
OSSpinLock 自旋锁
NSRecursiveLock 递归锁
NSLock 
dispatch_semaphore_t 信号量
NSCondition 条件
NSConditionLock 条件锁

简介:

  • @synchronized 使用场景:一般在创建单例对象时使用,保证对象在多线程中是唯一的。
  • atomic 属性关键字原子性,保证赋值操作是线程安全的,读取操作不能保证线程安全。
  • OSSpinLock 自旋锁。特点:循环等待访问,不释放当前资源。常用于轻量级数据访问,简单的int值+1/-1操作。
    * NSLock 某个线程A调用lock方法。这样,NSLock将被上锁。可以执行“关键部分”,完成后,调用unlock方法。如果,在线程A 调用unlock方法之前,另一个线程B调用了同一锁对象的lock方法。那么,线程B只有等待。直到线程A调用了unlock。
[lock lock]; //加锁
// 关键部分
[lock unlock]; // 解锁
  • NSRecursiveLock 递归锁,特点:递归锁在被同一线程重复获取时不会产生死锁。
  • dispatch_semaphore_t 信号量
    // 创建信号量结构体对象,含有一个int成员
    dispatch_semaphore_create(1);
    // 先对value减一,如果小于零表示没有资源可以访问。通过主动行为进行阻塞。
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // value加1,小于等零表示有队列在排队,通过被动行为进行唤醒
    dispatch_semaphore_signal(semaphore);
    

OSSpinLock

自旋锁,等待锁的线程会处于忙等状态,一直占用着CPU资源。

常用API:

导入头文件
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁
OSSpinLockTry(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

使用 OSSpinLock 解决卖票问题

// 自旋锁:#import <libkern/OSAtomic.h>
// 定义一个全局的自旋锁对象 lock 。
- (void)printTest2 {
    // 加锁
    OSSpinLockLock(&_lock);
    NSInteger oldCount = self.count;
    sleep(0.2);
    oldCount --;
    self.count = oldCount;
    NSLog(@"还剩%ld张票 - %@", (long)oldCount, [NSThread currentThread]);
    // 解锁
    OSSpinLockUnlock(&_lock);
}

使用 OSSpinLock 解决存取钱问题

- (void)saveMoney {
    OSSpinLockLock(&_moneyLock);
    int oldCount = self.moneyCount;
    sleep(0.2);
    oldCount += 50;
    self.moneyCount = oldCount;
    NSLog(@"存50,还剩%d钱", self.moneyCount);
    OSSpinLockUnlock(&_moneyLock);
}
- (void)takeMoney {
    OSSpinLockLock(&_moneyLock);
    int oldCount = self.moneyCount;
    sleep(0.2);
    oldCount -= 20;
    self.moneyCount = oldCount;
    NSLog(@"取20,还剩%d钱", self.moneyCount);
    OSSpinLockUnlock(&_moneyLock);
}

注意:卖票和取钱不要共用一把锁。这里创建了两把锁 sellLockmoneyLock

自旋锁现在不再安全,因为可能出现优先级反转问题。如果等待锁的线程优先级较高,他会一直占用CPU资源,优先级低的线程就无法获取CPU资源完成任务并释放锁。可以查看这篇文章 不再安全的OSSpinLock

本节示例代码: 线程同步解决方案Demo

os_unfair_lock

自旋锁已经不再安全,存在优先级反转问题。苹果在iOS10开始使用 os_unfair_lock 取代了 OSSpinLock 。从底层调用来看,自旋锁和 os_unfair_lock 的区别,前者等待线程处于忙等,而后者等待线程处于休眠状态。

常用API:

导入头文件 
#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(&_lock);
// 加锁
os_unfair_lock_lock(&_lock);
// 解锁
os_unfair_lock_unlock(&_lock);

pthread_mutex

互斥锁,等待锁的线程处于休眠状态。

常用API:

// 头文件 #import <pthread.h>
- (void)__initLock:(pthread_mutex_t *)lock {
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    // 设置为普通锁,PTHREAD_MUTEX_RECURSIVE表示递归锁
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    pthread_mutex_init(lock, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
}
// 加锁
pthread_mutex_lock(&lock);
// 解锁
 pthread_mutex_unlock(&lock);
// 初始化条件
pthread_cond_init(&cond, NULL)
// 等待条件(进入休眠,放开锁;被唤醒后,会再次加锁)
pthread_cond_wait(&cond, &lock);
// 激活一个等待该条件的线程
pthread_cond_signal(&cond);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&cond); 
// 销毁资源
pthread_mutex_destory(&lock);
pthread_cond_destory(&cond);

其中 PTHREAD_MUTEX_DEFAULT 设置的是锁的类型,还有另一种类型 PTHREAD_MUTEX_RECURSIVE 表示递归锁。递归锁允许同一个线程对一把锁进行重复加锁。

NSLock&NSRecursiveLock&NSCondition

NSLock 是对 mutex 普通锁的封装。

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking> {
- (BOOL)tryLock; // 尝试加锁
- (BOOL)lockBeforeDate:(NSDate *)limit; //在时间之前获取锁并返回,YES表示成功。
}
@end

NSRecursiveLock 是对 mutex 递归锁的封装,API同 NSLock 相似。

NSCondition 是对 mutex 条件的封装。

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSCondition : NSObject <NSLocking> {
- (void)wait; // 等待
- (BOOL)waitUntilDate:(NSDate *)limit; // 等待某一个时间段
- (void)signal; // 唤醒
- (void)broadcast; // 唤醒所有睡眠线程
}

以上可以查看 pthread_mutex 使用。

atomic

atomic 用于保证属性 settergetter 的原子性操作,相当于对 settergetter 内部加了同步锁。它并不能保证使用属性的使用过程是线程安全的。

NSConditionLock

NSConditionLock 是对 NSCondition 的进一步封装。可以设置具体的条件值。

// 遵循NSLocking协议。
@interface NSConditionLock : NSObject <NSLocking> {
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER; // 初始化,传入一个条件值

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition; // 条件值符合加锁
- (BOOL)tryLock; //尝试加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@end

示例代码:

// 删除
- (void)__one {

    // 当锁内部条件值为1时,加锁。
//    [self.condition lockWhenCondition:1];
    [self.condition lock]; // 直接使用lock也可以
    sleep(1);
    NSLog(@"%s ①", __func__);
    [self.condition unlockWithCondition:2]; // 解锁,并且条件设置为2
}
// 添加
- (void)__two {
    
    [self.condition lockWhenCondition:2]; //条件值为2时,加锁。
    sleep(1);
    NSLog(@"%s ②", __func__);
    [self.condition unlockWithCondition:3];
}

// 添加
- (void)__three {
    
    [self.condition lockWhenCondition:3]; //条件值为2时,加锁。
    sleep(1);
    NSLog(@"%s ③", __func__);
    [self.condition unlock];
}

- (void)otherTest {
    // ①
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    // ②
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    // ③
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
    
    // 通过设置条件值,可以决定线程的执行顺序。
}

输出结果:

-[LENSConditionLock

one] ①

two] ②

-[LENSConditionLock __three] ③

信号量

常用API:

// 初始化
 dispatch_semaphore_t semaphore = dispatch_semaphore_create(5);  
// 如果信号量的值<=0,当前线程就会进入休眠等待,直到信号量的值>0
// 如果信号量的值>0,就减1,然后往下执行后面的代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值增加1,信号量值不等于零时,前面的等待的代码会执行。
dispatch_semaphore_signal(self.semaphore);

dispatch_semaphore 信号量的初始值,控制线程的最大并发访问数量。

信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。

示例代码:

// 设置信号量初始值为5。
- (void)otherTest {
    for (int i = 0; i < 20; i ++) {
        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
    }
}
- (void)test {
    // 如果信号量的值<=0,当前线程就会进入休眠等待,直到信号量的值>0
    // 如果信号量的值>0,就减1,然后往下执行后面的代码
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    sleep(2);
    NSLog(@"test - %@", [NSThread currentThread]);
    // 让信号量的值增加1
    dispatch_semaphore_signal(self.semaphore);
}

@synchronized

@synchronized 是对 mutex 递归锁的封装。

不推荐使用,性能比较差。

// 源码:objc4中的objc-sync.mm
@synchronized (obj) {
}

性能比较

不再安全的OSSpinLock 中对比了不同锁的性能。

推荐使用 dispatch_semaphorepthread_mutex 两个。因为 OSSpinLock 性能最好但是不安全, os_unfair_lock 在iOS10才出现低版本不支持不推荐。

自旋锁、互斥锁的选择

自旋锁预计线程等待锁的时间很短,加锁经常被调用但竞争情况很少出现。常用于多核处理器。

互斥锁预计等待锁的时间较长,单核处理器。临界区有IO操作,例如文件读写。

示例代码: 锁实例代码-Github

小结

  • 怎样用GCD实现多读单写?
  • iOS提供几种多线程技术各自的特点?
  • NSOperation对象在Finished之后是怎样从队列中移除的?
  • 你都用过哪些锁?结合实际谈谈你是怎样使用的?

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK