CPU的执行"路径"就是"线程"
举例: <<食堂卖饭>>
食堂要卖饭, 至少要开一个卖饭的窗口, 这个窗口就是”线程”; 学生们要打饭, 就要拍成一条队列, 这条队列像一条线, 这条线, 就是”路径”.
“CPU命令列(二进制代码)”:
在Mac或iPhone上, “编译器”将我们写的”源码”, 转换成”CPU命令列(二进制代码)”, 这条”CPU命令列”很长很长, 先将它存放在内存里, 然后丢给CPU执行.
(翻译: 也就是说无论你代码怎么写, 最后都被”编译器”压成一根”线”, 然后CPU就从这一头一直读到另一头)
“命令列的地址”:
在不考虑出错的情况下, CPU可以从其中任意一个”位置”开始往下读. 而这个位置就叫”命令列的地址”.
(翻译: 把这根”线”存进内存里, 内存地址就成了这根”线”上的刻度)
“位置迁移”:
if/while/for等”控制语句”或”函数调用”等情况下, 是可以修改CPU的”执行命令列的地址”, 这就叫”位置迁移”.
(翻译: CPU每次都会去找它的”执行命令列的地址”, 例如”执行命令列的地址”=0, CPU就从0的位置一直往后读, 读到某个位子, 遇到名为”for”的”控制语句”, 这个”控制语句”将”执行命令列的地址”的值改成了0, 于是CPU又回到0开始往下读, 这就是for循环了.)
“路径”/”线程”:
1个CPU执行的CPU命令列为一条”无分叉路径”(1. 它是一条”路径”; 2. 它没有分叉), 这条”路径”, 就是”线程”.
(翻译: 你把自己想象成CPU, 你在一条名为”CPU命令列”的直线跑道上来回的跑, 这条跑道并不是”线程”, 你奔跑过程中产生的这条”路径”才是”线程”. 既然是奔跑路径, 那么这条”路径”就包括了你已经跑完的路径, 和你未跑完的路径. 所以什么是”线程”, 就是CPU的执行”路径”, 这条路径既包含了已经执行的路径, 也包含了将要执行路径)
关于”路径”, 再举一个例子: <<名为”CPU”的车库>>
CPU就像车库, 你就是上帝, 你画一条路径出来, CPU就必须安排一辆赛沿着这条路径跑, 你怎么画他怎么跑. 你画几条他就跑几条, 你画100条它就必须跑100条. 至于到底是你画的每一条路径都安排一辆赛车去跑, 还是安排一辆赛车同时跑多条路径, 这个由CPU决定.
假如1个CPU里有4个核心, 每个核心跑2个线程, 这就是4核8线程处理器.
ps: 物理意义上的4个核心和理论意义上的8个线程
*注意: 这里说的CPU的核心和线程, 与单个应用程序使用了多少”线程”是两码事, 但是就”线程”的概念而言, 是同一个概念.
举例: 就有点像赛车跑出来的路径和你人跑出来的路径, 它们在概念上都是路径, 你在草稿本上计算它们跑了多远, 都是画一根线, 但是在其他意义上, 它们又是有区别的, 比如赛车跑出来的路径它就比人跑出来的路径宽. 应用程序使用的”线程”都是从CPU的”线程”里往下继续细分出来的.
多条"路径"同时执行, 或多个"线程"同时运行, 就是"多线程".
假设只有1个CPU, 然后有2条”路径”名为”甲”和”乙”, 然后让这个CPU”同时”在2条”路径”上奔跑, 于是这就叫多线程了.
这里的”同时”是打引号的, 1个CPU怎么可能同时干2个CPU的事? 其实是通过快速切换”上下文切换”, 让CPU在”甲 - 路径”上跑一会, 又让CPU在”乙 - 路径”上跑一会, 因为切换的速度足够快, 于是你感觉是”同时”在跑.
假设有多个CPU, 分别执行不同的”路径”, 这就是”多线程”.
当你已经有了”路径”和”线程”概念以后, 就可以进行下面的拓展了
路径专用内存块: 系统在内存里划分了一块只供某一条”路径”专用的”内存块”,
*作用: 保存”路径的状态”, 如”CPU寄存器”等信息.
CPU寄存器: 在 “2. 线程 -> (3) if/while/for” 中我们用for循环讲解什么是”位置迁移”, 但是我们知道for循环每次把CPU”位置迁移”回起点的时候, for循环中的”局部变量”是会改变的, 而这些变量是存放在”CPU寄存器”中的.
上下文切换: CPU切换执行的”路径”的时候, 将当前”路径”的”路径的状态”存入”路径专用内存块”, 并根据目标”路径”的”路径专用内存块”中的数据复原”路径的状态”, 然后执行目标”路径”的”CPU命令列”, 这个过程, 就叫”上下文切换”.
例如, 从”甲 - 路径”切换到”乙 - 路径”,
1. 先将当前”CPU寄存器”等”路径的状态”都保存进了”甲 - 路径”的”路径专用内存块”中,
2. 再从”乙 - 路径”的”路径专用内存块”中取出数据复原”路径的状态”(“路径的状态”包含”CPU寄存器”等信息),
3. 然后CPU就可以执行”乙 - 路径”的”CPU命令列”了.
利用多线程技术的技术就叫"多线程编程".
(这句话有点像: 用筷子吃饭的技术就叫"筷子吃饭法")
应用程序在启动的时, 最先执行的那个那个"线程", 就是"主线程".
RunLoop 就是 "运行循环",
让一个"线程"即使没有了"路径"也依然能够存在下去,
并且响应一些"特定的事件".
"特定的事件": port, timer.
循环周期: 1/60 秒循环一遍.
从功能上看, RunLoop 更像是一个"线程托管器".
引用的例子: <<食堂卖饭>>
一个应用软件其实就是一个”进程”, 一个”进程”里面就有很多”线程”, “线程”就会去执行那些用”CPU命令列”画出来的一条”路径”, CPU负责在这条”路径”上奔跑, 既然是用”CPU命令列”画出来的”路径”, 那么这条”路径”就是有限的, 有的”路径”被CPU跑完之后就丢掉了, 对应的”线程”也随之关闭, 比如开一个新的”线程”读取文件信息, 读取完毕之后, 这条”线程”就关闭了.
问题来了, 我们开发的是APP, 你有见过哪个APP是打开以后, 马上就自动关闭关闭的吗? 没有!
那就说明, 这个APP至少有一个”主线程”是一直存在, 所以APP在它自身加载完毕之后不会立马关闭, 还能继续响应玩家的各种点击屏幕的事件操作.
为什么普通的”线程”执行完就会被关闭掉, 而”主线程”就不会呢? 因为”主线程”里面还有一个叫”运行循环”的东西存在, 这个东西的作用就是, 不让CPU关闭这条”线程”, 同时还能响应外部传入的”触摸”等事件, 并且根据不同的事件, 把CPU在这条”路径”上的”执行命令列的地址”调整到某个的位置继续执行, 既”位置迁移”.
*翻译:
你设计了一个软件, 软件里面有你对用户各种”触摸”事件设计的功能, 这些功能最终都被”编译器”压成一根根”路径”, 然后把这些”路径”插到一根”主路径”上, 看起来像棵没有长叶子的树 - “Ψ”.
你启动这个软件, 于是源文件被编译成”CPU命令列”存进内存, 然后CPU安排一辆赛车(“线程”), 先沿着树 - “Ψ”的主干跑一遍, 把你APP的UI界面都跑出来, 并且安排一个”汽车托管员”(RunLoop)来接管这个汽车(“主线程”).
结合本篇开头就提到的”路径”和”线程”的关系, 引用的例子: <<食堂卖饭>>
你就明白, 虽然”路径”已经走完了, 但是赛车(“线程”)还在, 只是驾驶员(一个”CPU核心”)跑去开其他车(其他”线程”)了. 这个车(“线程”)托管给了”汽车管理员”(RunLoop), 所以, 这个赛车(“线程”)虽然没了驾驶员, 但是赛车(“线程”)依然存在, 不会消失.
然后触摸屏传来了某个事件, 这个事件被”汽车管理员”(RunLoop)知道了, 于是”汽车管理员”(RunLoop)就把赛车”起点”调到某个支路的入口, 然后通知CPU这里有条”路径”需要跑, CPU接到通知以后, 安排一个驾驶员过来上车跑完这段”路径”, 然后停下来, 又把车丢给”汽车管理员”(RunLoop), 跑去开其他车了…
线程的扫盲就结束了, 下面进行GCD里的概念扫盲
英文全名 Grand Central Dispatch (GCD).
它是iOS和OS X众多异步执行任务的技术中的一个, 它只是其中一个!
好处: GCD可以让程序员远离了直接操作"线程", 而进行"多线程编程"的技术.
"处理"它
可以是"函数"(function),
可以是"方法"(method),
可以是"代码块"(block),
可以是他们的任意组合.
把许多"处理"像排队一样排列在一起, 然后按照"FIFO"的"追加的顺序"执行处理.
* 注意区分没 打引号的中文单词 处理, 和打了双引号的”处理”的区别, 打了双引号的”处理”代表了GCD里的一个概念, 就好比Java里的”对象”它不是跟你相处的对象(你女友, 你老婆), 它只是Java里的一个重要概念, 它是你脑壳里面的一个概念, 它是一个概念!**
"串行调用队列"里执行"处理"的规则是:
必须等待上一个"处理"处理完之后, 才会执行下一个"处理".
(这段话有点绕口,单词 执行 和 单词 处理 的含义不同:
执行"处理"不需要等待结果, 处理"处理"需要等待结果)
翻译: 你排队在我后面买车票, 售票员一定是先替我服务(执行), 服务过程有: 收下我的钱 - > 再给我车票(处理). 然后再服务(执行)你: 收下你的钱 -> 再给你车票. 然后再服务下一个人.
把”服务” , 我/你/人, “收钱 -> 给票” 这些词替换一下,
再翻译上面的话:
售票员先服务(执行)我("处理"), 并且处理完我("处理")之后,
再服务(执行)你("处理"), 处理完你("处理")之后, 再服务下一个人("处理").
有了: “线程”, 执行, 处理, “处理”, 这些概念之后, 来看”并行”
"并行调用队列"里执行"处理"的规则是:
不必等待 上一个"处理" 处理完毕(或者说: 处理结果),
就可以执行 这一个"处理", 执行完 这一个"处理"之后
也不必等待 这一个"处理"的处理结果,
就可以继续执行 下一个"处理"...
翻译: 学校组织体检, 你们各自拿着体检表排成一排, 护士按照排队顺序收下(执行)体检表, 然后你们进行体检(执行), 医生把你的体检数据记录在体检表上(执行), 体检完之后所有人都在等待拿回自己的体检表. 这个时候, 护士从医生那里拿了一堆体检表(执行), 点名点到谁就发给谁(执行), 被点到名的人就可以拿到自己的体检表(结果).
所以, 执行完一个”处理”不一定有结果(详见5.1), 处理完一个”处理”一定有结果. 而”并行调用队列”它只负责执行, 并且不需要等待处理结果.
在使用GCD的”多线程编程”过程中, 你会发现, 你一直在接触”并行队列”/”串行队列”, 那么请问, “多线程编程”里的”线程”哪去了呢?
那就是”线程”你不用管了, 由”XNU内核”替你管.
GCD就是一个让程序员远离了”线程”而进行”多线程编程”的技术.
举例: <<运营一所学校的食堂>>
已知: 一个学校(iOS或OS X), 有很多学生(“处理”), 有一个校长(XNU 内核), 有5个打饭阿姨(CPU 核心).
现在: 你(程序员)叫所有学生(“处理”)排队(“队列”①)打饭, 校长(XNU 内核)决定先开5个打饭窗口(那就是5个”线程”), 1个打饭阿姨(CPU 核心)负责1个打饭窗口(“线程”), 一段时间以后, 校长(XNU 内核)发现队伍排得太长了, 于是决定再开放5个打饭窗口(现在就是10个”线程”), 1个打饭阿姨(CPU 核心)负责2个打饭窗口(2个”线程”).
#注解:
①"队列"包含: "串行调用队列" 和 "并行调用队列"
通过这个例子, 可以反映出来:
(1) 程序员在使用GCD进行”多线程编程”的时候所扮演的jio色, 不是”线程”的管理者, 而是组织学生(“处理”)排队(“队列”)的保安(程序猿).
(2) “线程”的管理者由XNU 内核扮演.
掌握完以上概念之后, 你就可以思路非常轻松的利用GCD了
英语跟我一样差的请这样记: 是(sy)脑(n)残(c) = sync = 同步
常用函数: dispatch_sync(queue, NULL);
表示在当前"线程"执行"队列".
饿是脑残 = async
常用函数: dispatch_async(queue, NULL);
表示另开一个"线程"执行"队列".
等号 =
双引号 ” ”
单引号 ’ ’
逗号 ,
分号 ;
脱字符号 ^
在OC编程里, 你会经常接触和使用 脱字符号 ^,
OC里的脱字符号 ^ 表示这是一个 block;
例如:
function(int) 表示参数是一个int类型, 既整型.
function(int *) 表示参数是一个int *类型, 并且因为星号 *,
程序员一看就知道这是 指针类型.
method:(void(^)(void))aBlock 表示参数是一个block类型, 参数名叫aBlock, 并且因为脱字符号 ^,
程序员一看就知道这不是函数, 而是block;