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

多线程.png

在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] ①
-[LENSConditionLock __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之后是怎样从队列中移除的?
  • 你都用过哪些锁?结合实际谈谈你是怎样使用的?

参考

小码哥底层班视频
正确使用多线程同步锁@synchronized()
深入理解iOS开发中的锁
Object-C 多线程中锁的使用-NSLock

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,504评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,898评论 1 300
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,218评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,322评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,693评论 3 290
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,812评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,747评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,476评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,700评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,190评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,541评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,206评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,129评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,903评论 0 199
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,894评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,748评论 2 274

推荐阅读更多精彩内容

  • 台风“妮妲”掠过羊城,雨一阵一阵的,并不大,或许是这妮妲的眼睛太大,羊城今天很平静,上午晚些时候就去上班了。台风给...
    城市屋檐下阅读 313评论 0 2
  • AppLLM阅读 462评论 1 9
  • 无题 山青青,水寂寂,带着江南的烟雨,心思袅袅,亭亭而...
    拙兰阅读 386评论 5 12