iOS 多线程总结(下)

一、前言

继续我们上篇《iOS 多线程总结(上)》,继续总结多线程的其他知识点,希望帮助到更多伙伴。这篇主要总结一下线程同步方案,atomic 以及读写安全方案。

二、iOS 中的线程同步方案

  • 线程同步的意思就是让多线程的操作按顺序执行。
    方案有如下10 种:
    \color{blue}{OSSpinLock}自旋锁
    os_unfair_lock 自旋锁的替代品
    pthread_mutex
    dispatch_semaphore 信号量
    dispatch_queue(DISPATCH_QUEUE_SERIAL)串行队列
    \color{green}{NSLock}
    \color{green}{NSRecursiveLock}
    \color{green}{NSCondition}
    \color{green}{NSConditionLock} 条件锁
    \color{red}{@synchronized}

多个线程修改某个方法中同一个变量时,要用全局变量的锁,每个线程执行到这个方法时,会判断这个锁是否被加锁了,如果被加锁了则会等待锁被解锁再继续执行,所以必须使用全局变量的锁。
如果只在一个方法里使用了这把锁,也可以做成 static 类型的,这样也可以达到只初始化一次的效果。

1、OSSpinLock (自旋锁)

  • \color{purple}{OSSpinLock} 叫做”自旋锁“,等待锁的线程会处于忙等(busy-wait)状态,一直占用着 CPU 资源。
  • 目前已经不再安全,可能出现优先级反转问题。
  • 需要导入头文件<libkern/OSAtomic.h>

让线程停止有两种方法:
1、一直 while 判断等待,忙等;
2、sleep 休眠的方式;

\color{purple}{OSSpinLock} 自旋锁的优先级反转问题:

  • 如果等待锁的线程优先级较高,它会一直占用着 CPU 资源,优先级低的线程就无法释放。
    例子:
    有两个线程,
    thread1:优先级比较高
    thread2:优先级比较低
    如果优先级比较低的 thread2 先进了方法给锁进行了加锁,紧接着优先级比较高的 thread1 也进来这个方法了,发现这个锁已经被被人加过了,thread1 只能忙等,由于 thread1 的优先级比较高,cpu 就有可能一直在分配时间给 thread1,cpu 就没有时间再分配给 thread2 了,这时 thread2 的代码就没办法继续执行,就永远无法解锁,thread1 就会一直在等,类似于死锁了的感觉了。所以自旋锁有可能会有这种问题。
    如果采用休眠的方式的锁,则不会产生这个问题。

2、os_unfair_lock(互斥锁)

当我们使用 OSSpinLock 时,现在会报下面这个警告:

  • iOS10 开始,苹果希望我们使用 os_unfair_lock 来代替 OSSpinLock
  • 从底层调用看,等待 os_unfair_lock 锁的线程会处于休眠状态,并非忙等。
  • 需要导入头文件<os/lock.h>

从苹果的注释可以看到,这是个Low-level lock(简称ll lock 或lll),低级锁,低级锁特点就是等不到锁就休眠。

3、pthread_mutex 普通锁

  • mutex 叫做”互斥锁“,等待锁的线程会处于休眠状态
    pthread 开头的一般都是跨平台使用的锁。
  • 需要导入 <pthread.h>

属性默认是 PTHREAD_MUTEX_NORMAL,属性传空时也是这个默认的。
当属性传 PTHREAD_MUTEX_RECURSIVE 时,是递归锁。
递归锁:
允许\color{red}{同一个线程}对一把锁进行重复加锁。

  • 自旋锁原理

执行断点的时候,控制台输入step是一行一行 OC 代码执行(默认),如果输入stepi(缩写si也行),就是一行一行汇编指令执行。
nexti也是一行一行汇编执行,区别是遇到函数callq的时候,会一笔带过,不会进入函数。而 stepi 会进入函数。输入continue(缩写c也行)可以直接到下一个你打的断点。
输入完指令,输入回车即可。
在多个线程使用 OSSpinLock 自旋锁时,可以看到汇编代码一直在执行 0x111126a320x111126a41 这段代码,0x111126a43 的jne是jump判断,如果符合条件,则跳转到0x111126a32,这就是一个 while 循环,在忙等。

4、pthread_mutex - 递归锁

5、pthread_mutex - 条件锁

例子:
对一个数组的删除和添加操作分别在两个子线程,但是删除数组元素之前,我们需要先添加元素。

@interface NSConditionDemo()
@property (strong, nonatomic) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation NSConditionDemo

- (instancetype)init {
    if (self = [super init]) {
        self.condition = [[NSCondition alloc] init];
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 生产者-消费者模式

// 线程1
// 删除数组中的元素
- (void)__remove {
    [self.condition lock];
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    [self.condition unlock];
}

// 线程2
// 往数组中添加元素
- (void)__add {
    [self.condition lock];
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    // 信号
    [self.condition signal];
    // 广播
//    [self.condition broadcast];
    [self.condition unlock];
}

使用场景:
多线程之间的依赖问题。比如线程1依赖线程2做一些事情,再回到线程1做事情。

5、NSLock、NSRecursiveLock

\color{purple}{NSLock} 是对 C 语言mutex 普通锁的 OC 版本的封装。就是对 pthread_mutex 的属性传 PTHREAD_MUTEX_NORMAL 时的封装。

NSLock的方法

tryLock 是尝试加锁,并且不阻塞。
lockBeforeDate 是如果在某个时间前可以加锁成功,则加锁并返回YES,否则返回NO,阻塞。

NSRecursiveLock 也是对 mutex 递归锁的OC版本的封装,API 跟 NSLock 基本一致。
所以 mutex 和 NSLock 的性能其实是一样的,只不过 NSLock 是面向对象的而已。

6、NSCondition 条件锁

\color{purple}{NSCondition} 是对 C 语言的 pthread_mutex_tpthread_cond_tOC 面向对象的封装,既包含了锁,也包含了条件。

image.png

7、NSConditionLock 条件锁

\color{purple}{NSConditionLock} 是对 NSCondition 的进一步封装,可以设置具体的条件值。

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end

@implementation NSConditionLockDemo

- (instancetype)init {
    if (self = [super init]) {
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (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];
}

- (void)__one {
    [self.conditionLock lock];
    NSLog(@"__one");
    sleep(1);
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two {
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    sleep(1);
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three {
    [self.conditionLock lockWhenCondition:3];
    NSLog(@"__three");
    [self.conditionLock unlock];
}

上面代码就是 __three 依赖于__two,__two 依赖于__one。
所以当我们希望不同子线程之间是有顺序的时候,也可以用条件锁实现。

8、dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列

直接使用 GCD 的串行队列,也是可以实现线程同步的。


例子:

@interface SerialQueueDemo()
@property (strong, nonatomic) dispatch_queue_t ticketQueue;
@property (strong, nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation SerialQueueDemo

- (instancetype)init {
    if (self = [super init]) {
        self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
        self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)__drawMoney {
    dispatch_sync(self.moneyQueue, ^{
        [super __drawMoney];
    });
}

- (void)__saveMoney {
    dispatch_sync(self.moneyQueue, ^{
        [super __saveMoney];
    });
}

- (void)__saleTicket {
    dispatch_sync(self.ticketQueue, ^{
        [super __saleTicket];
    });
}

9、dispatch_semaphore

  • \color{purple}{semaphore} 叫做”信号量“;
  • 信号量的初始值,可以用来控制线程并发访问的最大数量;
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。


所以我们可以把线程最大并发数设置为1,这样就可以达到线程同步的目的了。

10、@synchronized

  • \color{purple}{synchronized} 是对 mutex 递归锁的封装。
    从代码简洁度讲,是最简单的一种方案。但我们在xcode敲这个关键字的时候是没有自动提示的,因为苹果不推荐我们使用它,因为它的性能比较差。
  • 源码查看:objc4 中的 objc-sync.mm 文件
    底层是一个哈希表,根据传进去的对象作为key,找到封装的mutex的唯一对应的一把锁,大括号开始时是加锁,大括号结束时解锁。
- (void)__drawMoney {
    @synchronized([self class]) {
        [super __drawMoney];
    }
}

- (void)__saveMoney {
    @synchronized([self class]) { // objc_sync_enter
        [super __saveMoney];
    } // objc_sync_exit
}

- (void)__saleTicket {
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    
    @synchronized(lock) {
        [super __saleTicket];
    }
}

- (void)otherTest {
    @synchronized([self class]) {
        NSLog(@"123");
        [self otherTest];
    }
}

iOS 线程同步方案性能比较

  • 性能从高到低排序
    os_unfair_lock —— iOS10 以后才可以用
    OSSpinLock ——已经不推荐使用
    dispatch_semaphore ——信号量
    pthread_mutex
    dispatch_queue(DISPATCH_QUEUE_SERIAL)——串行队列
    NSLock ——对 pthread_mutex 的封装
    NSCondition ——条件
    pthread_mutex(recursive) ——递归锁
    NSRecursiveLock ——递归锁,对 pthread_mutex(recursive) 的封装
    NSConditionLock ——条件锁
    @synchronized ——性能最差,但代码最简洁
    所以最推荐 dispatch_semaphorepthread_mutex

💡小技巧:
使用信号量的时候,如果我们 3 个方法需要用不同的锁,那么我们除了可以在外面定义三个不同的锁属性,还可以在每个方法内部定义静态的锁,这样就能保证一个方法一个锁了。

static dispatch_semaphore_t semaphore;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    semaphore = dispatch_semaphore_create(1);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

// ... 要加锁的代码...

dispatch_semaphore_signal(semaphore);

更简便的是进行宏定义:
- (void)test1 {
    SemaphoreBegin;
    // .....要加锁的代码...
    SemaphoreEnd;
}

- (void)test2 {
    SemaphoreBegin;
    // .....要加锁的代码...
    SemaphoreEnd;
}

- (void)test3 {
    SemaphoreBegin;
    // .....要加锁的代码...
    SemaphoreEnd;
}
  • 什么情况使用自旋锁比较划算?
    1、预计线程等待锁的时间段;
    2、加锁的代码(临界区)经常被调用,但竞争情况很少发生;
    3、CPU资源不紧张;
    4、多核处理器;

  • 什么情况使用互斥锁比较划算?
    1、预计线程等待锁的时间较长;
    2、单核处理器;
    3、临界区有IO操作;
    4、临界区代码复杂或者循环量大;
    5、临界区竞争非常激烈(很多线程抢占资源);
    但在 iOS 里不考虑使用自旋锁了,只用互斥锁即可。

三、 atomic

nonatomic 和 atomic

atom:原子,不可再分割的单位
atomic:原子性

给属性加上 atomic 修饰,可以保证属性的 settergetter 都是原子性操作,也就是保证 settergetter 内部是线程安全的。

可以通过 objc4 源码中的 objc-accessors.mm 查看,objc_getPropertyreallySetProperty
reallySetProperty 中:

objc_getProperty中

  • atomic 用于保证属性 setter、getter 的原子性操作,相当于在 gettersetter 内部加了线程同步的锁。
  • 它不能保证使用属性的过程是线程安全的。
    iOS 由于性能问题,一般不使用 atomic,在 mac OS 使用会更多些。

四、iOS 中的读写安全方案

如果实现以下场景:

  • 同一时间,只能有1个线程进行写的操作
  • 同一时间,允许有多个线程进行读的操作
  • 同一时间,不允许既有写的操作,又有读的操作
    上面的场景就是典型的”多读单写“,经常用于文件等数据的读写操作,iOS中的实现方案有:
    pthread_rwlock读写锁
    dispatch_barrier_async异步栅栏调用

pthread_rwlock

  • 等待锁的线程会进入休眠


dispatch_barrier_async

  • 这个函数传入的并发队列必须是自己通过 dispatch_queue_cretate 创建的
  • 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async 函数的效果

以上的总结参考了并部分摘抄了以下文章,非常感谢以下作者的分享!:
小马哥-李明杰的《多线程》课程

转载请备注原文出处,不得用于商业传播——凡几多

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

推荐阅读更多精彩内容