KM的博客.

iOS多线程

字数统计: 3.9k阅读时长: 14 min
2018/06/01

深入理解GCD

dispatch_async 会把任务添加到队列的一个链表中,添加完后会唤醒队列,根据 vtable 中的函数指针,调用 wakeup 方法。

  • 在 wakeup 方法中,从线程池里取出工作线程(如果没有就新建),然后在工作线程中取出链表头部指向的 block 并执行。

dispatch_sync 的实现略简单一些,它不涉及线程池(因此一般都在当前线程执行),而是利用与线程绑定的信号量来实现串行。

分发到不同队列时,代码进入的分支也不一样,比如 dispatch_async 到主队列的任务由 runloop 处理,而分发到其他队列的任务由线程池处理。

在当前串行队列中执行 dispatch_sync 时,由于 dq_running 属性(表示在运行的任务数量) 为 1,所以以下判断成立:

1
2
3
if (slowpath(!dispatch_atomic_cmpxchg2o(dq, dq_running, 0, 1))) {  
return _dispatch_barrier_sync_f_slow(dq, ctxt, func);
}

_dispatch_barrier_sync_f_slow 函数中使用了线程对应的信号量并且调用 wait 方法,从而导致线程死锁。

如果向其它队列同步提交 block,最终进入 _dispatch_barrier_sync_f_invoke,它只是保证了 block 执行的原子性,但没有使用线程对应的信号量。

对于信号量来说,它主要使用 signalwait 这两个接口,底层分别调用了内核提供的方法。

  • 在调用 wait 方法后,先将 value 减一,如果大于零立刻返回,否则陷入等待。signal 方法将信号量加一,如果 value 大于零立刻返回,否则说明唤醒了某一个等待线程,此时由系统决定哪个线程的等待方法可以返回。

dispatch_group 的本质就是一个 value 非常大的信号量,等待 group 完成实际上就是等待 value 恢复初始值。

  • notify 的作用是将所有注册的回调组装成一个链表,在 dispatch_async 完成时判断 value 是不是恢复初始值,如果是则调用 dispatch_async 异步执行所有注册的回调。

dispatch_once 通过一个静态变量来标记 block 是否已被执行,同时使用信号量确保只有一个线程能执行,执行完 block 后会唤醒其他所有等待的线程。

dispatch_barrier_async 改变了 block 的 vtable 标记位,当它将要被取出执行时,会等待前面的 block 都执行完,然后在下一次循环中被执行。

dispatch_source 可以用来实现定时器。

  • 所有的 source 会被提交到用户指定的队列,然后提交到 manager 队列中,按照触发时间排好序。
  • 随后找到最近触发的定时器,调用内核的 select 方法等待。
  • 等待结束后,依次唤醒 manager 队列和用户指定队列,最终触发一开始设置的回调 block。

GCD 中的对象用 do_suspend_cnt 来表示是否暂停。队列默认处于启动状态,而 dispatch_source 需要手动启动。

dispatch_after 函数依赖于 dispatch_source 定时器,它只是注册了一个定时器,然后在回调函数中执行 block。

GCD死锁案例分析

NSOperation案例分析

深入理解iOS开发中的锁

自旋锁的目的是为了确保临界区只有一个线程可以访问

1
2
3
4
5
6
7
bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {
while(test_and_set(&lock); // test_and_set 是一个原子操作
Critical section // 临界区
lock = false; // 相当于释放锁,这样别的线程可以进入临界区
Reminder section // 不需要锁保护的代码
}
  • 显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的。
  • 如果临界区的执行时间过长,使用自旋锁不是个好主意

信号量

  • 首先会把信号量的值减一,并判断是否大于零。
  • 如果大于零,说明不用等待,所以立刻返回。小于0等待signal唤醒线程

pthread_mutex互斥锁

  • 互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
1
2
3
4
5
6
7
8
9
10
pthread_mutexattr_t attr;  
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定义锁的属性

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 创建锁

pthread_mutex_lock(&mutex); // 申请锁
// 临界区
pthread_mutex_unlock(&mutex); // 释放锁
  • 一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。
  • 假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
  • 然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可。

NSLock

  • NSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。

  • NSLockpthread_mutex 略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

NSCondition

  • NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。

  • 条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程****,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。

  • NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者

  • 它的加解锁过程与 NSLock 几乎一致,理论上来说耗时也应该一样(实际测试也是如此)。在图中显示它耗时略长,我猜测有可能是测试者在每次加解锁的前后还附带了变量的初始化和销毁操作。

NSRecursiveLock

  • 递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

    使用递归锁NSRecursiveLock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^RecursiveMethod)(int);
    RecursiveMethod = ^(int value) {
    [lock lock];//递归调用时失败,NSLock不能被同一线程多次获取,多次获取使用NSRecursiveLock
    if (value > 0) {
    NSLog(@"value = %d", value);
    sleep(2);
    RecursiveMethod(value - 1);
    }
    [lock unlock];
    };
    RecursiveMethod(5);
    });

@synchronized

这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。

  1. 你调用 sychronized 的每个对象,runtime 都会为其分配一个递归锁并存储在哈希表中。
  2. 如果在 sychronized 内部对象被释放或被设为 nil 看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。
  3. 注意不要向你的 sychronized block 传入 nil!这将会从代码中移走线程安全。你可以通过在 objc_sync_nil 上加断点来查看是否发生了这样的事情。

1、你理解的多线程?并发和串行,同步和异步

  • 同步和异步的区别: 是否开辟新的线程,同步只能在当前线程执行任务,异步可以再新的线程执行任务
  • 串行和并发的区别:是任务执行的顺序,串行任务只能顺序执行,并发可以多个任务同时执行。

2、iOS多线程有哪些?常用哪个?

  • NSThread、GCD/NSOperationQueue
  • 常用GCD/NSOperation

pthread NSThread GCD NSoperatio

3、GCD 的队列类型有哪些?

GCD的队列可以分为2大类型

  • 并发队列(Concurrent Dispatch Queue)
    • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    • 并发功能只有在异步(dispatch_async)函数下才有效
  • 串行队列(Serial Dispatch Queue)
    • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)

4、OperationQueue 和 GCD 的区别?

  1. GCD是底层的C语言构成的API,而NSOperationQueue及相关对象是Objc的对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构;而Operation作为一个对象,为我们提供了更多的选择;
  2. 在NSOperationQueue中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而GCD没法停止已经加入queue的block(其实是有的,但需要许多复杂的代码);
  3. NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;
  4. 我们能将KVO应用在NSOperation中,可以监听一个Operation是否完成或取消,这样子能比GCD更加有效地掌控我们执行的后台任务;
  5. 在NSOperation中,我们能够设置NSOperation的priority优先级,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;
  6. 我们能够对NSOperation进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将block任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。

总的来说,Operation queue 提供了更多你在编写多线程程序时需要的功能,并隐藏了许多线程调度,线程取消与线程优先级的复杂代码,为我们提供简单的API入口。

从编程原则来说,一般我们需要尽可能的使用高等级、封装完美的API,在必须时才使用底层API。但是我认为当我们的需求能够以更简单的底层代码完成的时候,简洁的GCD或许是个更好的选择,而Operation queue 为我们提供能更多的选择。

5、线程安全是什么?如何处理线程安全的问题?

我们一般通过线程同步方案如加锁的方式来实现线程的安全

iOS锁的原理 | 深入浅出iOS系统内核-同步机制

  • os_unfair_lock:iOS10开始os_unfair_lock来替代OSSpinLock, 等待锁的线程会进入休眠不占用CPU资源, 这个os_unfair_lock解决了优先级翻转问题。
  • OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等一直占用着CPU资源
  • dispatch_semaphore:信号量为1的semaphore也可以看做是锁
  • pthread_mutex:跨平台的互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
  • 使用GCD的串行队列也可以实现锁的功能
  • NSLock是对metex互斥锁的封装,NSRecurisiveLock也是对metex的递归封装,API和NSLock一直。
  • NSCondition
  • NSConditionLock
  • @synchorized也是mutex的递归封装,@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。

6、自旋锁和互斥锁如何选择?

自旋锁OSSpinLock:当线程获取锁,其他等待锁的线程会忙等一直占用CPU,自旋锁有优先级翻转的可能性,所以苹果已经弃用自旋锁OSSpinLock改用os_unfair_lock.

互斥锁mutext:阻塞线程并休眠,其他线程才能正常访问,用互斥的方式来保证线程的安全。

递归锁:顾名思义,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。

  • 什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 竞争情况很少发生,加锁的代码(临界区)经常被调用
    • CPU资源不紧张 多核处理器
  • 什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈
    • 单核处理器 临界区有IO操作
  • 追问二:使用以上锁需要注意哪些?

  • 追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!

7、iOS线程同步方案性能比较

性能从高到低排序:

1
os_unfair_lock > OSSpinLock > dispatch_semaphore > pthread_mutex > dispatch_queue(DISPATCH_QUEUE_SERIAL) > NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > NSConditionLock > @synchronized

**8、如何用gcd实现并发执行1和2再执行任务3的方案?

  • 异步并发执行任务1、任务2
  • 等任务1、任务2都执行完毕后,再回到主线程执行任务3

dispatch_group_notify

dispatch_barrier_async

dispatch_sempher(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  let group = DispatchGroup()
let queue = DispatchQueue.init(label: "handleAPIQueue")
group.enter()
queue.async {
print("任务1完成")
group.leave()
}

group.enter()
queue.async {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("任务2完成")
group.leave() //注意leave的位置必须在任务完成后
}
}
//group.leave() //如果leave放在这里的话,notify不会等待任务2完成就会触发

group.enter()
queue.async {
print("任务3完成")
group.leave()
}

group.notify(queue: queue) {
print("所有任务都完成了")
}

网络请求应用实例

9、如何实现多度单写?

dispatch_barrier_async

  • 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
  • 如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  dispatch_queue_t concurrentQueue = dispatch_queue_create("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"async_1");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"async_2");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"async_3");
});
dispatch_barrier_async(concurrentQueue, ^{//dispatch_barrier_sync效果相同
NSLog(@"dispatch_barrier_async");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"async_4");
});
/*
01-OC底层[25009:1007376] async_1
01-OC底层[25009:1007372] async_3
01-OC底层[25009:1007374] async_2
01-OC底层[25009:1007372] dispatch_barrier_async
01-OC底层[25009:1007372] async_4
*/

10、GCD实现暂停和继续注意什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
		dispatch_queue_t conQueue = dispatch_queue_create("com.seria.queue", DISPATCH_QUEUE_CONCURRENT);
size_t count = 5;
dispatch_apply(5, conQueue, ^(size_t index) {
NSLog(@"numer is %zu",index);
if (index == 2) {
// dispatch_suspend(concurrentQueue);// suspend并不能停止当前队列的任务,只能停止后面队列中的任务
}
});
dispatch_suspend(conQueue); // dispatch_suspend 不能单独使用,和dispatch_resume配对使用
NSLog(@"task1---");

dispatch_async(conQueue, ^{
NSLog(@"async1");
});
NSLog(@"task2---");
dispatch_resume(conQueue);

dispatch_async(conQueue, ^{
NSLog(@"async2");
});
NSLog(@"task3---");

11、案例分析

使用 Dispatch Source定时器

Dispatch Source Timer

利用 Dispatch Source 的 DISPATCH_SOURCE_TYPE_TIMER 类型,我们可以创建一个 跨线程的 定时器(我们平时使用的 NSTimer 是基于 Run Loop 的 timer 事件,只能在对应的线程里触发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t queue = dispatch_get_main_queue();

//1、创建一个 timer;
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

//2、配置 timer,从现在起,每两秒在主线程触发一次,精度为0s
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

//3、timer 触发之后的回调 block
dispatch_source_set_event_handler(self.timer, ^{

NSLog(@"%ld", self.count++);
});

//4、启动 timer
dispatch_resume(self.time);
CATALOG
  1. 1. 深入理解GCD
  2. 2. 深入理解iOS开发中的锁
    1. 2.0.1. 自旋锁的目的是为了确保临界区只有一个线程可以访问
    2. 2.0.2. 信号量
    3. 2.0.3. pthread_mutex互斥锁
    4. 2.0.4. NSLock
    5. 2.0.5. NSCondition
    6. 2.0.6. NSRecursiveLock
    7. 2.0.7.
    8. 2.0.8. @synchronized
  • 3. 1、你理解的多线程?并发和串行,同步和异步
  • 4. 2、iOS多线程有哪些?常用哪个?
  • 5. 3、GCD 的队列类型有哪些?
  • 6. 4、OperationQueue 和 GCD 的区别?
  • 7. 5、线程安全是什么?如何处理线程安全的问题?
  • 8. 6、自旋锁和互斥锁如何选择?
  • 9. 7、iOS线程同步方案性能比较
  • 10. **8、如何用gcd实现并发执行1和2再执行任务3的方案?
    1. 10.0.1. dispatch_group_notify
    2. 10.0.2. dispatch_barrier_async
    3. 10.0.3. dispatch_sempher(2)
  • 11. 9、如何实现多度单写?
  • 12. 10、GCD实现暂停和继续注意什么?
  • 13. 11、案例分析
    1. 13.0.1. 使用 Dispatch Source定时器
    2. 13.0.2. Dispatch Source Timer