iOS探索 - 多线程之相关原理

文章搬运来源:https://juejin.cn/post/6924912442682114062#heading-17
作者:小峰子

对iOS开发感兴趣,可以看一下作者的iOS交流群:812157648,大家可以在里面吹水、交流相关方面的知识,群里还有我整理的有关于面试的一些资料,欢迎大家加群,大家一起开车

写在前面

多线程是比较庞大和单独的模块,所以此多线程系列计划从四个方面入手, 包括原理篇应用篇底层篇面试篇。本文主要讲诉多线程相关的原理,后续文章依次讲诉其他。

1.进程和线程

1.1 什么是进程

进程是指在系统中正在运行的一个可执行文件,每个进程之间都是相互独立的,每个进程运行在其专用且受保护的内存。

需要注意

我们常说iOS单进程的。这对也不对。

如果真是单进程,完全没有后台,那想想推送是怎么被收到的,电话是怎么被唤醒的,就解释不通了。熟悉逆向的同学,就知道有不少的后台在开机后就在运行了。

所以严格来说,应该是对于应用程序来说,iOS是单进程的

至于为什么iOS采用单进程

因为多进程间的切换会消耗大量资源,并且使用沙盒机制,保证系统更流畅安全的运行。

1.2 什么是线程

线程进程的基本单元,进程中的所有任务都在线程中执行,进程至少要有一条线程才能执行任务。程序启动会默认开启一条线程,称为主线程

从技术角度来看,线程是管理代码执行所需的内核级和应用程序级数据结构的组合。内核级结构在一个可用内核上协调事件向线程的调度。应用程序级别的结构包括用于存储函数调用的调用堆栈,以及应用程序管理和操纵线程的属性和状态所需的结构。

简单来说,线程依赖于内核级的调度来完成应用程序级的任务

1.3 进程和线程的关系

地址空间:同一个进程线程共享本进程的地址空间,不同进程的地址空间是互相独立的。

资源拥有:同一个进程线程共享本进程的资源,如内存,I/O,cpu等,不同进程之间的资源是互相独立的。

异常处理:一个进程崩溃后,不会对另一个进程产生影响;一个线程崩溃后,对应的进程也会崩溃。

执行过程:每个独立的进程都有应用程序入口,顺序执行序列;线程不能独立执行,必须依赖于进程。一个线程中的任务是串行,同一时间,一个线程只能执行一个任务,所以线程进程的一条执行路径。

1.4 队列和线程的关系

队列是一种满足先进先出(FIFO)结构的运算受限的特殊线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。

队列线程没有关系,却又总是一起出现。就像银行窗口不关心队伍是怎么排列的,只负责做业务。

队列负责任务执行的顺序,线程负责任务的具体执行

1.5 runloop和线程的关系

  • runloop线程是一一对应的,一个runloop对应一个核心线程
  • 线程runloop被开启后,线程在执行完任务会进入休眠状态,任务来了会被唤醒执行
  • runLoop在第一次被获取时创建,销毁则是在线程结束的时候
  • 主线程的runLoop在程序启动时默认创建好了,而子线程的runLoopd需要我们主动创建和维护

2.多线程

2.1 多线程和单线程

单线程

在非并行应用程序中,只有一个执行线程在执行任务。该线程以应用程序的main例程开始和结束,并且一个一个地分支到不同的方法或函数,以实现应用程序的整体行为。

可以理解成一家银行(进程),只开通一个窗口(线程)在办理业务(执行任务),并且至少需要一个窗口(线程)。

多线程

多线程时,即支持并发的应用程序从一个线程开始,并根据需要添加更多线程以创建其他执行路径。每个新路径都有其自己的自定义启动例程,该例程独立于应用程序main例程中的代码运行。

可以理解成一家银行(进程),开通多个窗口(线程)在办理业务(执行任务)。

2.2 多线程的原理

对于单核设备,多线程并发执行,其实是CPU快速地在多条线程之间调度,时间片在不停切换,如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。对于多核设备,才是真正意义的多线程

这两种情况分别叫做多任务多处理

多任务多处理是相关的概念,但却不相同。多任务处理是一次处理多个不同任务的能力。多处理是计算机同时使用多个处理器的能力。通常,如果在多任务环境中有多个处理器可用,则可以在处理器之间分配任务。在这种情况下,任务可以同时运行。

如果线程非常非常多,会发生什么情况?

CPU会在N多线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低,导致程序效率降低。

2.3 多线程的利弊

多线程的利

  • 多线程可以提高应用程序的感知响应能力
  • 多线程可以提高应用程序在多核系统上的实时性能
  • 多线程可以适当提高资源的利用率

多线程的弊

  • 多线程会增加程序的复杂度,需要考虑线程之间的通信、多线程的数据共享等,出错可能性增加
  • 如果开启大量的线程,会占用大量的内存空间
  • 线程越多,CPU在调度线程上的开销就越大

2.4 线程的生命周期

新建线程后,线程以三种主要状态之一运行:运行就绪阻塞。如果线程当前未在运行,则要么阻塞并等待输入,要么准备运行。线程在这些状态之间来回移动,直到最终退出并进入死亡状态。

  • 新建:创建线程实例
  • 就绪:向线程发出star消息,并把线程加入可调度线程池
  • 运行:被cpu调度执行任务时
  • 堵塞线程被堵塞,如调用sleep,同步锁,栅栏函数等
  • 死亡:任务执行完毕,并且没有新任务需要执行,或者线程被强制退出

新建线程时,必须为该线程指定一个入口点函数。该入口点函数构成要在线程上运行的代码。当函数返回时,或当强制退出线程时,线程将永久停止并被系统回收。由于就内存和时间而言,创建线程的成本相对较高,因此建议入口点函数进行大量工作或设置运行循环以允许重复执行工作,而非新建多条线程执行

退出线程的最佳方法自然是让线程到达其主入口点例程的末尾。尽管有强制退出线程的功能,但这些功能仅应作为最后的手段使用。在线程到达其自然终点之前强制退出该线程可能导致难以预料的后果。如果线程已分配内存,打开文件或获取其他类型的资源,则可能无法回收这些资源,从而导致内存泄漏或其他潜在问题。

2.5 线程池原理

corePoolSize:核心线程数
maxPoolSize:最大线程数
复制代码
  1. 如果小于corePoolSize, 就创建线程并执行该任务;否则,将该任务放入阻塞队列;
  2. 如果能成功将任务放入阻塞队列中, 如果当前线程池是非运行状态,则将该任务从阻塞队列中移除,然后执行reject()处理该任务;如果当前线程池处于运行状态,则需要再次检查线程池(因为可能在上次检查后,有线程资源被释放),是否有空闲的线程;如果有则执行该任务;
  3. 如果不能将任务放入阻塞队列中,说明阻塞队列已满;那么尝试创建一个新的线程去执行这个任务;如果执行失败,说明线程池中线程数达到maxPoolSize,则执行reject()处理任务;

线程池的饱和策略:当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

  • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,阻止系统正常运行
  • CallerRunsPolicy:将任务回退到调用者
  • DiscardOldestPolicy:丢弃等待最久的任务,然后重新尝试执行任务(重复此过程)
  • DisCardPolicy:直接丢弃任务

当然也可以实现RejectedExecutionHandler接口,自定义饱和策略,如任务日志记录。

2.6 多线程的技术方案

方案 简介 语言 生命周期
pthread 通用API,跨平台,使用难度大 C 程序员管理
NSThread 面向对象,简单易用,可直接操作线程 OC 程序员管理
GCD 充分利用设备的多核 C 自动管理
NSOperation 面向对象,基于GCD,功能更多 OC 自动管理

基于手动管理线程生命周期很繁琐,而且容易出错,因此应尽可能避免使用pthreadNSThread。所以GCDNSOperation的使用率更高。

2.7 任务执行的效率

影响任务执行的效率包括:

  • 线程数和cpu的调度速度
  • 任务的复杂度
  • 任务的优先级

2.6 线程安全之库开发

不少同学日常会封装一些发布在同性交友网站,或提供团队使用。

封装过程中应注意,尽管可以控制应用程序是否使用多线程执行,但是却不能。开发时,必须假定调用者是多线程的,或者可以随时切换为多线程。因此,应该始终对代码的关键部分使用

线程编程的危害之一是多个线程之间的资源争用。如果多个线程尝试同时使用或修改同一资源,则可能会出现问题。不能完全维护单独的资源时,必须使用条件原子操作和其他技术来同步对资源的访问。

对于,仅当应用程序成为多线程时才创建是不明智的。如果需要在某个时候锁定代码,请在使用的早期创建锁定对象,最好是通过某种显式调用来初始化

广为流传的一定是线程安全的。

2.7 多线程的通讯

  • 直接通讯:直接在其他线程上执行选择器的功能,参考performSelectorOnMainThread相关的API
  • 全局变量:尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用或其他同步机制保护共享变量,以确保代码的正确性
  • 条件锁:等待条件的线程将保持阻塞状态,直到另一个线程显式通知该条件为止。参考NSCondition
  • runloop源:比如设置timer
  • 端口:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术,它也是基于runloop的,参考NSPort
  • 消息队列:消息队列是任务必须按先进先出顺序处理的数据(消息)的集合。参考NSNotificationQueue

写在后面

多线程是提高程序效率的开发利器,但它不应该被滥用产生难以定位的错误。合理使用多线程,人人有责。

你可能感兴趣的:(iOS探索 - 多线程之相关原理)