iOS学习笔记

WWDC Grand Central Dispatch 的现代

2021-03-06  本文已影响0人  DingGa

WWDC Grand Central Dispatch 的现代化用法
本篇内容来自https://developer.apple.com/wwdc17/706, 纯属笔记,方便自己要是忘记了,后面好查阅

GCD for concurrency

代码如何同时在多个不同的内核上并行执行?
并发是关于你如何构造 你应用的独立组件使其同时运行,并行通常是需要多内核支持的,你想同时使用全部的内核 而并发甚至可以在单核系统中实现,它是关于你如何介入 作为应用的一部分的不同任务 那么让我们从并行开始谈 以及当你在写应用时如何使用它
举例,让我们想象一下你正在开发一款应用 而它会处理大量图片 你希望能利用Mac Pro上的多核 来更快地处理这些图片


image

这就提升了你的速度 因为多核同时 处理图片的不同部分

而我们如何去做这个事情呢?

concurrentPerform

GCD表达并行的模式是使用一个 叫作concurrentPerform的API

DispatchQueue.concurrentPerform(1000) { i in /* iteration i */ }

DispatchQueue.concurrentPerform 和 Swift 3.0 之前的
dispatch_apply(DISPATCH_APPLY_AUTO, 1000, ^(size_t i){ /* iteration i */ }) 是一样的,

GCD 中实现并行的优化

这里循环的次数是一个比较微妙的数字,比如如果我们有一个任务拆成 3 段并行执行,然后全部执行结束后,对外输出结果,则其运行效果可能是这样的:


image

这里你可以看到理想情况 也就是三个代码块在全部 三个内核上并行运行

但现实世界总是不会这么完美
如果第三个内核 被占用执行UI渲染会发生什么呢?

image

负载平衡器必须得把那第三个代码块 挪到第一个内核上 以便执行第三个代码块 因为第三个内核被占用了 我们的CPU闲置 我们可以利用这段时间 来执行更多的并行工作 那么相反 我们的工作花的时间更长

image
image

可以使用足够大的重复计数 以便负载平衡器可以灵活地 填补系统中的空白 并最大限度地利用 系统中可用的资源, 但是, 请记住每个CPU并不总是全部可用 系统中同时还运行着许多任务,我们无法为了追求项目最大并发而影响到其他任务.

concurrency

我们来想像一下,我们写一个简单的新闻应用.
他有 用户信息, 网络,和数据几个层面.


image

我们从运行任务来看一下多线程


image

UI 在主线程中进行,其拥有最高的优先级,在cpu内某个次级线程在执行任务的时候,如果发生了用户操作,则该次线程有可能被打断执行高优操作。并发机制几乎在所有平台,所有语言都有很好的支持,他的好处是毋庸置疑的:

1.可以提高资源利用率, 机器上我们谈到资源,一般指的是 CPU,但通常情况下网络或磁盘的 I/O 要比内存 I/O 慢的多,所以我们在执行频繁 I/O 的任务时,CPU 很多时候都处于闲置状态。这时如果我们开启多个线程,在 A 线程 I/O 的同时让 CPU 执行 B,在 B 线程 I/O 的同时再执行 A。这样就比 AB 串行执行时 CPU 的利用率更高。
2.响应更快,我们在主线程接受用户请求后,将耗时操作交给子线程,然后告诉用户在等待的同时还可以干点别的。

image

我们看看对于CPU来说 是什么样的
上边的这些白线 显示的是子系统之间的内容切换

可以使用仪表系统进行追踪 可以给你显示CPU和线程正在做什么 当它们在你的应用中运行时 如果你想了解更多信息 你可以参看去年的 “深度解析系统追踪”演讲 仪表团队描述了你该如何使用系统追踪 image

并发的缺点

过多的线程间切换,就像下面这样,白块都是用于线程间的资源消耗,有个 task 可能只需要 20 us,但是线程间切换就需要 10 us。

并发和锁

和多线程伴生最多的概念就是锁(Lock)了, 多线程间难免对同一个资源感兴趣,为了保证资源的状态有序,我们需要对资源的读和写进行控制:

NSLock 最基本的互斥锁
NSRecursiveLock 递归锁,可以被一个线程多次获得,而不会引起死锁。
NSCondition 条件,包含一个锁和一个线程检查器。
NSConditionLock 条件锁, 不需要检查器,条件锁自带一个探测条件,是否满足
pthread_mutex POSIX 互斥锁是一种超级易用的互斥锁,比较熟悉用的较多。
pthread_rwlock 读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的
OSSpinLock 自旋锁,和互斥锁类似,都是为了保证线程安全的锁。但二者的区别是不一样的,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自旋锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。
os unfair lock apple为了解决优先级反转提供的锁,下面会单独讲一下。
dispatch_semaphore 信号量机制实现锁. 用的也是比较多的。
@synchronized 一个便捷的创建互斥锁的方式,它做了其他互斥锁所做的所有的事情。性能堪忧。
image image

锁的拥有者: 像 pthread mutex,os unfair lock,锁住的资源同时只能被一个线程拥有,其他都需等待。而像信号量(dispatch semaphore),条件(nscondition)锁住的资源并不归属某个线程拥有,满足了条件的线程会被允许通过,read 或 write 资源。而像 pthread_rwlock,可能有多个线程同时拥有这个共享资源的读权限。

先来看 fair_lock。多个线程同时竞争这个锁得时候, 会考虑公平性尽可能的让不同的线程公平。 这个公平其实是有很大的性能损失换来的。


image

在上面的图中,锁先被线程 1 持有,然后释放,被线程 2 持有,引起一次上下文切换。


image
使用锁会发生线程锁定,持有资源后,低优先级的任务可能会出现在高优先级前面.
image

重复地在不同的操作 (operations)中切换

举例每一个连接都对应一个串行队列。当网络发生变化时,这些队列中的代码都要被执行。
假如队列形式如下,S 表示网络发生变化, Q 表示网络变化时代码要执行的串行队列。


image

当网络变化时,会有 3 个队列生成,对应三个线程。


image

这里就会发生上下文切换


image

但是如果是指定3个任务在同一个队列中的话.


image

在网络发生变化时,这三个队列的代码将在同一个 queue 中执行,不会有上下文切换。


image
此外,如果把太多的任务加到全局并行队列,也会导致生成太多线程。
苹果推荐的方法是,每一个子系统(数据库、网络。。。)有一个队列层级,如下图:
image

统一队列标识符 (Unified Queue Identity)

大概是每一个队列都在内核中有一个标志符,可以根据这个标识符做优化。
在之前,把两个 source 的队列设置为一个,当处理 S1 时, S2 被触发,那么会新建一个线程。


image

经过优化之后,不会创建一个额外的线程了。
大致原理是当 S2 触发时,系统知道对应的队列正在执行,于是做了一个标记。
当处理完毕之后,再进行相应处理。


image

如何处理代码

在 activate 函数调用之后,不要改变 source handler 和 target queue。因为系统会创建一个快照,根据这个快照来优化调度。

let mySource = DispatchSource.makeReadSource(fileDescriptor: fd, queue: myQueue)
mySource.setEventHandler(qos: .userInteractive) { … }
mySource.setCancelHandler { close(fd) }
mySource.activate()
mySource.setTarget(queue: otherQueue)//wrong!!!!!!!!!!!!!!!! 错误实例,不要乱来哦

Serial Dispatch Queue 串行队列

Dispatch Source

Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。我们来看看它都有哪些类型:

Timer Dispatch Source:定时调度源。
Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
Process Dispatch Source:监听进程相关状态的调度源。
Mach port Dispatch Source:监听Mach相关事件的调度源。
Custom Dispatch Source:监听自定义事件的调度源。

用GCD的函数指定一个希望监听的系统事件类型,再指定一个捕获到事件后进行逻辑处理的闭包或者函数作为回调函数,然后再指定一个该回调函数执行的Dispatch Queue即可,当监听到指定的系统事件发生时会调用回调函数,将该回调函数作为一个任务放入指定的队列中执行。也就是说当监听到系统事件后就会触发一个任务,并自动将其加入队列执行

Diaptach Source与Dispatch Queue关联后,只要监听到系统事件,Dispatch Source就会自动将任务(回调函数)添加到关联的队列中。有些时候回调函数执行的时间较长,在这段时间内Dispatch Source又监听到多个系统事件,理论上就会形成事件积压,但好在Dispatch Source有很好的机制解决这个问题,当有多个事件积压时会根据事件类型,将它们进行关联和结合,形成一个新的事件。

保护好队列的层级

在创建队列时指定 target queue。

Q1 = dispatch_queue_create_with_target("Q1", DISPATCH_QUEUE_SERIAL, EQ)
//下面被淘汰啦  
Q1 = dispatch_queue_create("Q1",DISPATCH_QUEUE_SERIAL)
dispatch_set_target_queue(Q1, EQ)

使用新的工具

Instrument 带有 GCDPerformance 工具,可以诊断问题。

结束.

上一篇下一篇

猜你喜欢

热点阅读