半路出家, 我的iOS自学之路-5-GCD的概念, 线程, RunLoop, 处理, 队列, 串行(xing), 并行(xing), 同步(sync), 异步(async)

半路出家, 我的iOS自学之路-5-GCD的概念, “线程”, RunLoop, “处理”, “队列”, “串行(xing)”, “并行(xing)”, “同步(sync)”, “异步(async)”, “FQDN(fully qualified domain name)”, “FIFIO”

  • 只学过Java, 半路出家, 自学iOS.
  • 以下是我读完《Objective-C 高级编程》iOS与OS X多线程和内存管理的读书笔记
  • 博客中出现任何差错, 遗漏, 还请您在评论中告诉我
  • 群号:(空), 欢迎自学iOS的人加入, 一起交流, 共同成长

基础概念

线程概念扫盲

1. 线程

CPU的执行"路径"就是"线程"

举例: <<食堂卖饭>>
食堂要卖饭, 至少要开一个卖饭的窗口, 这个窗口就是”线程”; 学生们要打饭, 就要拍成一条队列, 这条队列像一条线, 这条线, 就是”路径”.

  1. 光有卖饭的窗口, 没有排队打饭的学生: 开一个”线程”, 没有可执行的”路径”, 这种”线程”是无意义的, 属于资源浪费.
  2. 光有排队打饭的学生, 没有卖饭的窗口: 内存里有可以执行的”路径”, 没有”线程”去执行, 这也算是内存泄漏了, 属于资源浪费.
  3. 所以简单而不精确的归纳总结就是: CPU的执行”路径”就是”线程”.
  4. 那么到底什么是”线程”呢? 请先想想, 到底什么是”卖饭的窗口”, 你也没有真实看见, 我也只是随便假设有这么一个”卖饭的窗口”, 我也没说”卖饭的窗口”长什么样子, 但是你的脑子里就是立马有了一个”卖饭的窗口”这样的概念, 所以, 到底什么是”线程”呢, “线程”就是用来执行”CPU命令列”的, 而那些等着被执行的”CPU命令列”就像一个一个的人一样排着队, 它们排队的整体形状就是一条”路径”.

1.1 CPU命令列, 位置迁移, 路径

  1. “CPU命令列(二进制代码)”:
    在Mac或iPhone上, “编译器”将我们写的”源码”, 转换成”CPU命令列(二进制代码)”, 这条”CPU命令列”很长很长, 先将它存放在内存里, 然后丢给CPU执行.

    (翻译: 也就是说无论你代码怎么写, 最后都被”编译器”压成一根”线”, 然后CPU就从这一头一直读到另一头)

  2. “命令列的地址”:
    在不考虑出错的情况下, CPU可以从其中任意一个”位置”开始往下读. 而这个位置就叫”命令列的地址”.

    (翻译: 把这根”线”存进内存里, 内存地址就成了这根”线”上的刻度)

  3. “位置迁移”:
    if/while/for等”控制语句”或”函数调用”等情况下, 是可以修改CPU的”执行命令列的地址”, 这就叫”位置迁移”.

    (翻译: CPU每次都会去找它的”执行命令列的地址”, 例如”执行命令列的地址”=0, CPU就从0的位置一直往后读, 读到某个位子, 遇到名为”for”的”控制语句”, 这个”控制语句”将”执行命令列的地址”的值改成了0, 于是CPU又回到0开始往下读, 这就是for循环了.)

  4. “路径”/”线程”:
    1个CPU执行的CPU命令列为一条”无分叉路径”(1. 它是一条”路径”; 2. 它没有分叉), 这条”路径”, 就是”线程”.

    (翻译: 你把自己想象成CPU, 你在一条名为”CPU命令列”的直线跑道上来回的跑, 这条跑道并不是”线程”, 你奔跑过程中产生的这条”路径”才是”线程”. 既然是奔跑路径, 那么这条”路径”就包括了你已经跑完的路径, 和你未跑完的路径. 所以什么是”线程”, 就是CPU的执行”路径”, 这条路径既包含了已经执行的路径, 也包含了将要执行路径)

    关于”路径”, 再举一个例子: <<名为”CPU”的车库>>
    CPU就像车库, 你就是上帝, 你画一条路径出来, CPU就必须安排一辆赛沿着这条路径跑, 你怎么画他怎么跑. 你画几条他就跑几条, 你画100条它就必须跑100条. 至于到底是你画的每一条路径都安排一辆赛车去跑, 还是安排一辆赛车同时跑多条路径, 这个由CPU决定.

    1. 假如1个CPU里有4个核心, 每个核心跑1个线程, 这就是4核4线程处理器.
      ps: 物理意义上: 4个核心天然对应了4个线程.
    2. 假如1个CPU里有4个核心, 每个核心跑2个线程, 这就是4核8线程处理器.
      ps: 物理意义上的4个核心和理论意义上的8个线程

      *注意: 这里说的CPU的核心和线程, 与单个应用程序使用了多少”线程”是两码事, 但是就”线程”的概念而言, 是同一个概念.
      举例: 就有点像赛车跑出来的路径和你人跑出来的路径, 它们在概念上都是路径, 你在草稿本上计算它们跑了多远, 都是画一根线, 但是在其他意义上, 它们又是有区别的, 比如赛车跑出来的路径它就比人跑出来的路径宽. 应用程序使用的”线程”都是从CPU的”线程”里往下继续细分出来的.

2. 多线程

多条"路径"同时执行, 或多个"线程"同时运行, 就是"多线程". 
  1. 假设只有1个CPU, 然后有2条”路径”名为”甲”和”乙”, 然后让这个CPU”同时”在2条”路径”上奔跑, 于是这就叫多线程了.

    这里的”同时”是打引号的, 1个CPU怎么可能同时干2个CPU的事? 其实是通过快速切换”上下文切换”, 让CPU在”甲 - 路径”上跑一会, 又让CPU在”乙 - 路径”上跑一会, 因为切换的速度足够快, 于是你感觉是”同时”在跑.

  2. 假设有多个CPU, 分别执行不同的”路径”, 这就是”多线程”.

当你已经有了”路径”和”线程”概念以后, 就可以进行下面的拓展了

2.1 “路径专用内存块”, “路径的状态”, “CPU寄存器”, “上下文切换”

  1. 路径专用内存块: 系统在内存里划分了一块只供某一条”路径”专用的”内存块”,
    *作用: 保存”路径的状态”, 如”CPU寄存器”等信息.

  2. CPU寄存器: 在 “2. 线程 -> (3) if/while/for” 中我们用for循环讲解什么是”位置迁移”, 但是我们知道for循环每次把CPU”位置迁移”回起点的时候, for循环中的”局部变量”是会改变的, 而这些变量是存放在”CPU寄存器”中的.

  3. 上下文切换: CPU切换执行的”路径”的时候, 将当前”路径”的”路径的状态”存入”路径专用内存块”, 并根据目标”路径”的”路径专用内存块”中的数据复原”路径的状态”, 然后执行目标”路径”的”CPU命令列”, 这个过程, 就叫”上下文切换”.

    例如, 从”甲 - 路径”切换到”乙 - 路径”,
    1. 先将当前”CPU寄存器”等”路径的状态”都保存进了”甲 - 路径”的”路径专用内存块”中,
    2. 再从”乙 - 路径”的”路径专用内存块”中取出数据复原”路径的状态”(“路径的状态”包含”CPU寄存器”等信息),
    3. 然后CPU就可以执行”乙 - 路径”的”CPU命令列”了.

2.2 多线程编程

利用多线程技术的技术就叫"多线程编程". 
(这句话有点像: 用筷子吃饭的技术就叫"筷子吃饭法")

3. 主线程

应用程序在启动的时, 最先执行的那个那个"线程", 就是"主线程".

4. NSRunLoop

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里的概念扫盲


GCD概念扫盲

1. GCD

英文全名 Grand Central Dispatch (GCD). 
    它是iOS和OS X众多异步执行任务的技术中的一个, 它只是其中一个!
好处: GCD可以让程序员远离了直接操作"线程", 而进行"多线程编程"的技术.

2. “处理”

"处理"它 
    可以是"函数"(function), 
    可以是"方法"(method), 
    可以是"代码块"(block), 
        可以是他们的任意组合.

3. “队列(queue)”

把许多"处理"像排队一样排列在一起, 然后按照"FIFO"的"追加的顺序"执行处理.
  • “FIFO” (全名: First-In-First-Out): 先进先出; 就跟你排队买票, 先排队的先买票.

3.1. “串行调用队列(Serial Dispatch Queue)”

* 注意区分没 打引号的中文单词 处理, 和打了双引号的”处理”的区别, 打了双引号的”处理”代表了GCD里的一个概念, 就好比Java里的”对象”它不是跟你相处的对象(你女友, 你老婆), 它只是Java里的一个重要概念, 它是你脑壳里面的一个概念, 它是一个概念!**

"串行调用队列"里执行"处理"的规则是:  
    必须等待上一个"处理"处理完之后, 才会执行下一个"处理". 
    (这段话有点绕口,单词 执行 和 单词 处理 的含义不同:  
        执行"处理"不需要等待结果, 处理"处理"需要等待结果)
  • 翻译: 你排队在我后面买车票, 售票员一定是先替我服务(执行), 服务过程有: 收下我的钱 - > 再给我车票(处理). 然后再服务(执行)你: 收下你的钱 -> 再给你车票. 然后再服务下一个人.

    把”服务” , 我/你/人, “收钱 -> 给票” 这些词替换一下,

    1. ”服务”: 执行
    2. 我/你/人: 等待执行的”处理”
    3. “收钱 -> 给票”: 处理完毕(既有过程也有结果)

    再翻译上面的话:

    售票员先服务(执行)我("处理"), 并且处理完我("处理")之后,  
        再服务(执行)你("处理"), 处理完你("处理")之后, 再服务下一个人("处理").
    

有了: “线程”, 执行, 处理, “处理”, 这些概念之后, 来看”并行”

3.2. “并行调用队列(Concurrent Dispatch Queue)”

"并行调用队列"里执行"处理"的规则是:  
    不必等待 上一个"处理" 处理完毕(或者说: 处理结果),  
        就可以执行 这一个"处理", 执行完 这一个"处理"之后
            也不必等待 这一个"处理"的处理结果, 
                就可以继续执行 下一个"处理"...
  • 翻译: 学校组织体检, 你们各自拿着体检表排成一排, 护士按照排队顺序收下(执行)体检表, 然后你们进行体检(执行), 医生把你的体检数据记录在体检表上(执行), 体检完之后所有人都在等待拿回自己的体检表. 这个时候, 护士从医生那里拿了一堆体检表(执行), 点名点到谁就发给谁(执行), 被点到名的人就可以拿到自己的体检表(结果).

    所以, 执行完一个”处理”不一定有结果(详见5.1), 处理完一个”处理”一定有结果. 而”并行调用队列”它只负责执行, 并且不需要等待处理结果.

4. “并行执行” 与 XNU 内核 以及 程序员在GCD”多线程编程”中所扮演的jio色

在使用GCD的”多线程编程”过程中, 你会发现, 你一直在接触”并行队列”/”串行队列”, 那么请问, “多线程编程”里的”线程”哪去了呢?
那就是”线程”你不用管了, 由”XNU内核”替你管.

  • iOS和OS X的核心都是XNU 内核.
  • XNU 内核根据”当前系统的状态”决定当前应当使用的线程数.
      • ”当前系统的状态”包含: 1. “处理”数; 2. CPU核数; 3.CPU负荷; 4. 等等…
  • “并行执行”多个”处理”的时候, 开几个”线程”, 每个”线程”下面放几个处理, 这些琐事都由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了

5. “同步(sync)”

英语跟我一样差的请这样记: 是(sy)脑(n)残(c) = sync = 同步

常用函数: dispatch_sync(queue, NULL);
    表示在当前"线程"执行"队列".

6. “异步(async)”

饿是脑残 = async

常用函数: dispatch_async(queue, NULL);
    表示另开一个"线程"执行"队列".

代码演示

符号讲解

等号 =
双引号 ” ”
单引号 ’ ’
逗号 ,
分号 ;
脱字符号 ^

在OC编程里, 你会经常接触和使用 脱字符号 ^, 
    OC里的脱字符号 ^ 表示这是一个 block;

例如: 
    function(int) 表示参数是一个int类型, 既整型.

    function(int *) 表示参数是一个int *类型, 并且因为星号 *, 
        程序员一看就知道这是 指针类型. 

    method:(void(^)(void))aBlock  表示参数是一个block类型, 参数名叫aBlock, 并且因为脱字符号 ^,
        程序员一看就知道这不是函数, 而是block;

以上内容算是从线程的基础概念, 一直扫盲到GCD相关概念, 至于实际用法, 因为涉及到”数据竞争”, “死锁”, “阻塞”, “内存过耗”, “线程安全”, “全局调用队列优先级”, “指定队列优先级”, “FQDN”, “脱字符号^”


你可能感兴趣的:(ios,ios,线程,异步,读书笔记,RunLoop)