当前位置: 首页 > news >正文

【iOS】多线程原理

目录

前言

基本概念及原理

线程、进程与队列

线程的定义:

进程的定义:

线程与进程之间的联系与区别:

线程和runloop的关系

影响任务执行速度的因素

多线程

多线程生命周期

线程池的原理

iOS中多线程的实现方式

线程安全问题

互斥锁

自旋锁

对比GCD和NSOperation

NSThread

GCD

函数

队列

函数与队列的不同组合

串行队列 + 同步派发

​编辑串行队列 + 异步派发

​编辑

并发队列 + 同步派发

​编辑

并发队列 + 异步派发

主队列 + 同步函数

主队列 + 异步派发

dispatch_after

dispatch_once

dispatch_apply

dispatch_group_t

dispatch_group_async + dispatch_group_notify

dispatch_group_enter + dispatch_group_leave + dispatch_group_notify

dispatch_barrier_sync & dispatch_barrier_async

dispatch_semaphore_t

dispatch_source

NSOperation

NSInvocationOperation

NSBlockOperation

NSOperationQueue

设置优先级

设置并发数

设置依赖


前言

多线程在iOS的开发中起到了非常重要的作用,笔者在之前已经有学习过关于GCD的知识了,但是当时学得迷迷糊糊,正好借学习多线程底层原理的机会,来在学习多线程的同时对之前的知识做一个复习

基本概念及原理

线程、进程与队列

线程的定义:
  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行

  • 进程想要执行任务,必须得有线程,进程至少要有一条线程

  • 程序启动会默认开启一条线程,这条线程被成为主线程UI线程

进程的定义:
  • 进程是指在系统中正在运行的一个应用程序,如微信、支付宝app都是一个进程

  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内

通俗地说,可以理解为:进程是线程的容器,而线程用来执行任务。在iOS中是单进程开发,一个进程就是一个app,进程之间是相互独立的,如支付宝、微信、qq等,这些都是属于不同的进程。

线程与进程之间的联系与区别:
  • 地址空间:同一进程线程共享本进程的地址空间,而进程之间则是独立的地址空间

  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的

  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮

  • 进程切换时,消耗的资源大、效率高.所以设计到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程而不能用进程

  • 线程是处理器调度的基本单位,但进程不是

  • 线程没有地址空间,线程包含在进程地址空间中

线程和runloop的关系
  • runloop与线程是一一对应的 —— 一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里

  • runloop是来管理线程的 —— 当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务

  • runloop在第一次获取时被创建,在线程结束时被销毁

    • 对于主线程来说,runloop在程序一启动就默认创建好了

    • 对于子线程来说,runloop是懒加载的 —— 只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调

影响任务执行速度的因素

以下因素都会对任务的执行速度造成影响:

  • cpu的调度

  • 线程的执行速率

  • 队列情况

  • 任务执行的复杂度

  • 任务的优先级

多线程

多线程生命周期

多线程的生命周期主要分为5部分:新建 - 就绪 - 运行 - 阻塞 - 死亡

  • 新建:实例化线程对象

  • 就绪:线程对象调用start方法,将线程对象加入可调度线程池等待CPU的调用,即调用start方法,并不会立即执行,进入就绪状态,需要等待一段时间,经CPU调度后才执行,也就是从就绪状态进入运行状态

  • 运行:CPU 负责调度可调度线程池中线程的执行。在线程执行完成之前,其状态可能会在就绪和运行之间来回切换.就绪和运行之间的状态变化由CPU负责,程序员不能干预

  • 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)

  • 死亡:正常死亡,即线程执行完毕。非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)

处于运行中的线程拥有一段可以执行的时间(称为时间片):

  • 如果时间片用尽,线程就会进入就绪状态队列

  • 如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列

  • 等待事件发生后,线程又会重新进入就绪状态队列

  • 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列选择一个线程继续执行

关于线程的exit和cancel方法:

  • exit:一旦强行终止线程,后续的所有代码都不会执行

  • cancel:取消当前线程,但是不能取消正在执行的线程

线程池的原理

可以看到主要分为以下四步:

  1. 判断核心线程池是否都正在执行任务:

    • 返回NO,创建新的工作线程去执行

    • 返回YES,进行第二步

  2. 判断线程池工作队列是否已经饱满:

    • 返回NO,将任务存储到工作队列,等待CPU调度

    • 返回YES,进入第三步

  3. 判断线程池中的线程是否都处于执行状态

    • 返回NO,安排可调度线程池中空闲的线程去执行任务

    • 返回YES,进入第四步

  4. 交给饱和策略去执行,主要有以下四种:

    • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行

    • CallerRunsPolicy:将任务回退到调用者

    • DisOldestPolicy:丢掉等待最久的任务

    • DisCardPolicy:直接丢弃任务

iOS中多线程的实现方式

iOS中多线程的实现方式主要有四种:pthread、NSThread、GCD、NSOperation

线程安全问题

当多个线程同时访问一块内存,容易引发数据错乱和数据安全问题,有以下两种解决方案:

  • 互斥锁(即同步锁):@synchronized

  • 自旋锁

互斥锁
  • 保证锁内的代码,同一时间,只有一条线程能够执行!

  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!

  • 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠

  • 能够加锁的是任意 NSObject 对象,但必须是 NSObject 对象

  • 锁对象必须保证所有线程都能访问

  • 单点加锁时推荐使用 self

自旋锁
  • 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态

  • 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本身就有一把自旋锁

  • 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能

  • atomic 本身就有一把锁(自旋锁)

iOS开发的建议:

  • 所有属性都声明为 nonatomic

  • 尽量避免多线程抢夺同一块资源 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

对比GCD和NSOperation

GCDNSOperation的关系如下:

  • GCD是面向底层的C语言的API

  • NSOperation是用GCD封装构建的,是GCD的高级抽象

GCD和NSOperation的对比如下:

  • GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构 —— 写起来更加方便

  • GCD只支持FIFO的队列,而NSOpration可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序

  • NSOpration甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier任务才能控制执行顺序,较为复杂

  • NSOperation支持KVO(面向对象)可以检测operation是否正在执行、是否结束、是否取消(如果是自定义的NSOperation 子类,需要手动触发KVO通知)

NSThread

NSthread是苹果官方提供面向对象的线程操作技术,是对thread的上层封装,比较偏向于底层。

通过NSThread创建线程的方式主要有以下三种方式:

  • 通过init初始化方式创建

  • 通过detachNewThreadSelector构造器方式创建

  • 通过performSelector...方法创建,主要是用于获取主线程,以及后台线程

NSThread常用的类方法有以下:

  • currentThread:获取当前线程

  • sleep...:阻塞线程

  • exit:退出线程

  • mainThread:获取主线程

GCD

GCD就是Grand Central Dispatch,它是纯 C 语言。关于GCD,笔者之前已经有博客详细介绍过它的概念和接口以及用法了。这里对于GCD的简单概念就不重复赘述了,详情可以点击笔者这篇博客——OC高级编程之GCD。这里就直接对GCD的函数与队列来进行一个再梳理和复习吧

函数

GCD中执行任务的方式有两种,同步执行异步执行,分别对应同步函数dispatch_sync异步函数dispatch_async

  • 同步执行,对应同步函数dispatch_sync

    • 必须等待当前语句执行完毕,才会执行下一条语句

    • 不会开启线程,即不具备开启新线程的能力

    • 在当前线程中执行block任务

  • 异步执行,对应异步函数dispatch_async

    • 不用等待当前语句执行完毕,就可以执行下一条语句

    • 会开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关)

    • 异步是多线程的代名词

综上所述,两种执行方式的主要区别有两点:

  • 是否等待队列的任务执行完毕

  • 是否具备开启新线程的能力

队列

多线程中所说的队列Dispatch Queue)是指执行任务的等待队列,即用来存放任务的队列.队列是一种特殊的线性表,遵循先进先出(FIFO)原则,即新任务总是被插入到队尾,而任务的读取从队首开始读取.每读取一个任务,则动队列中释放一个任务。而队列又分为串行队列并发队列

串行队列:每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个,即只开启一个线程

并发队列:一次可以并发执行多个任务,即开启多个线程,并同时执行任务

函数与队列的不同组合

串行队列 + 同步派发

任务一个接一个地在当前线程执行,不会开辟新线程

串行队列 + 异步派发

任务一个接一个地执行,但是会开辟新线程

并发队列 + 同步派发

任务一个接一个地执行,不开辟线程

并发队列 + 异步派发

任务乱序进行并且会开辟新线程

主队列 + 同步函数

任务互相等待,造成死锁

为什么这样会造成死锁,这里分析一下原因:

主队列在执行任务执行到同步block时,会将block的任务加入到主队列,但由于主队列是串行队列,因此block的任务要等主线程执行完block才可以执行(因为当前主线程中任务还没有执行完,任务应该是进行到执行block了),而执行block其实就是执行block里的任务(即NSLog),主线程等着block里这个任务执行完才执行完,这样就使得任务之间互相等待,从而造成了死锁崩溃

死锁

  • 主线程因为同步函数的原因等着先执行任务

  • 主队列等着主线程的任务执行完毕再执行自己的任务

  • 主队列和主线程相互等待会造成死锁

主队列 + 异步派发

主队列是一个特殊的串行队列,它虽然是串行队列,但是其异步派发不会开辟新线程,而是将任务安排到主线程的下一个运行循环(Run Loop)周期执行

dispatch_after

dispatch_after表示在队列中的block延迟执行,确切地说是延迟将block加入到队列

dispatch_once

dispatch_once可以保证在app运行期间,block中的代码只执行一次,可以用来创建单例

dispatch_apply

dispatch_apply将指定的block追加到指定的队列中重复执行,并等到全部的处理执行结束(相当于线程安全的for循环)

应用场景:在拉取网络数据后提前计算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性

dispatch_group_t

dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间

应用场景:多个接口请求之后刷新页面

dispatch_group_async + dispatch_group_notify

dispatch_group_notifydispatch_group_async执行结束之后会受收到通知

dispatch_group_enter + dispatch_group_leave + dispatch_group_notify

dispatch_group_enterdispatch_group_leave成对出现,使进出组的逻辑更加清晰

在此基础上还可以使用 dispatch_group_wait

这里dispatch_group_wait这个函数第一个参数表示要等待的调度组,第二个参数表示要等多久(如果设置为DISPATCH_TIME_NOW表示不等待直接判定是否执行完毕,如果设置为DISPATCH_TIME_FOREVER表示阻塞当前调度组直到调度组执行完毕)

这个函数的返回值为long类型,如果返回值为0,表示在指定时间内调度组完成了任务;如果不为0,表示在指定时间内调度组没有按时完成任务

dispatch_barrier_sync & dispatch_barrier_async

栅栏函数,主要使用在并发队列,串行队列使用栅栏函数没什么意义。

栅栏函数即:等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中。 简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务

可以看到如果没有栅栏,按照主线程的派发顺序,任务2延迟1s,任务1延迟2s,应该是先完成任务2再完成任务1的,但是因为这里有栅栏函数,所以这里任务执行的顺序变为:先执行栅栏前的任务1,再执行栅栏任务,然后执行栅栏后的任务2

  • dispatch_barrier_syncdispatch_barrier_async的作用相同,区别在于是否阻塞线程

注意⚠️

1.尽量使用自定义的并发队列

  • 使用全局队列起不到栅栏函数的作用

  • 使用全局队列时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)

2.栅栏函数只能控制同一并发队列:打个比方,平时在使用AFNetworking做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列(也就是说栅栏函数不能跨队列作用)

dispatch_semaphore_t

dispatch_semaphore_t表示信号量,可以用来控制GCD最大并发数

  • dispatch_semaphore_create():创建信号量

  • dispatch_semaphore_wait():等待信号量,信号量减1。当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到信号(signal)才执行下去

  • dispatch_semaphore_signal():释放信号量,信号量加1。当信号量>= 0 会执行wait之后的代码

比如用信号量来代替栅栏函数使这段代码按序输出:

使用信号量的API来改写的话就是这样的:

如果当创建信号量时传入值为1又会怎么样呢?

  • i=0时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为0不会阻塞线程,所以进入i=1

  • i=1时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为-1阻塞线程,等待signal再执行下去

结论:

  • 创建信号量时传入值为1时,可以通过两次才堵塞

  • 传入值为2时,可以通过三次才堵塞

dispatch_source

dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件

  • Timer Dispatch Source:定时器事件源,用来生成周期性的通知或回调

  • Signal Dispatch Source:监听信号事件源,当有UNIX信号发生时会通知

  • Descriptor Dispatch Source:监听文件或socket事件源,当文件或socket数据发生变化时会通知

  • Process Dispatch Source:监听进程事件源,与进程相关的事件通知

  • Mach port Dispatch Source:监听Mach端口事件源

  • Custom Dispatch Source:监听自定义事件源

主要使用的API:

  • dispatch_source_create: 创建事件源

  • dispatch_source_set_event_handler: 设置数据源回调

  • dispatch_source_merge_data: 设置事件源数据

  • dispatch_source_get_data: 获取事件源数据

  • dispatch_resume: 继续

  • dispatch_suspend: 挂起

  • dispatch_cancle: 取消

比如通过dispatch_source来实现定时器,在开发中经常使用NSTimer来实现定时逻辑,但是NSTimier是依赖Runloop的,而Runloop可以运行在不同的模式下,如果NSTimer添加在一一种模式下,而Runloop运行在其他模式下,定时器就挂起了;又如果Runloop在阻塞状态,那么NSTimer的触发时间就会推迟到下一个Runloop周。因此NSTimer在计时上会有误差,而GCD计时器不依赖Runloop,计时精度高很多

需要注意⚠️:

  • GCDTimer需要强持有,否则出了作用域立即释放,也就没有了事件回调

  • GCDTimer默认是挂起状态,需要手动激活

  • GCDTimer没有repeat,需要封装来增加标志位控制

  • GCDTimer如果存在循环引用,使用weak+strong或者提前调用dispatch_source_cancel取消timer

  • dispatch_resumedispatch_suspend调用次数需要平衡

  • source挂起状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在激活状态下调用dispatch_source_cancel(source)释放当前的source

NSOperation

NSOperation是个抽象类,依赖于子类NSInvocationOperationNSBlockOperation去实现

NSInvocationOperation

也可以直接处理事务,不添加隐性队列

NSBlockOperation

NSInvocationOperationNSBlockOperation两者的区别在于:

  • 前者类似target形式

  • 后者类似block形式——函数式编程,业务逻辑代码可读性更高

NSOperationQueue是异步执行的,所以任务一任务二的完成顺序不确定

通过addExecutionBlock这个方法可以让NSBlockOperation实现多线程

NSBlockOperation创建时block中的任务是在主线程执行,而运用addExecutionBlock加入的任务是在子线程执行的(准确地来说,创建时block中的任务在start调用发生的线程执行)(当Operation没有添加到队列,而是通过start调用时)

NSOperationQueue

NSOperationQueue有两种队列:主队列、其他队列

  • 主队列:主队列上的任务是在主线程执行的

  • 其他队列(非主队列):加入到非主队列中的任务默认就是并发,开启多线程

通过类方法mainQueue可以得到主队列

设置优先级

NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成

通过以下是否让高优先级任务休眠的任务执行顺序即可看出这一点:

不使用sleep:

使用sleep:

设置并发数

在NSOperation中不需使用信号量,直接设置maxConcurrentOperationCount就可以控制并发数,来控制单次出队列去执行的任务数

设置依赖

NSOperation中添加依赖能很好的控制任务执行的先后顺序

http://www.lryc.cn/news/622329.html

相关文章:

  • 昇腾AI自学Day1-- 深度学习基础工具与数学
  • C语言基础08——文件的输入与输出
  • git clone https://gh.llkk.cc/
  • 什么才是真正的白盒测试?
  • 高并发接口性能优化实战:从200ms到20ms的蜕变之路
  • Python正则表达式处理Unicode字符完全指南:从基础到高级实战
  • Python工具箱系列(六十四)
  • Java Lambda表达式是什么,怎么用
  • JavaWeb开发_Day12
  • 研学智得AI-知网推出的AI学术文献阅读工具
  • OpenCV---morphologyEx形态学操作
  • Java中MybatisPlus使用多线程多数据源失效
  • Vue 侦听器(watch 与 watchEffect)全解析3
  • 如何在 FastAPI 中玩转 APScheduler,让任务定时自动执行?
  • 快速了解PCA降维
  • 《Python列表和元组:从入门到花式操作指南》
  • 接口自动化测试步骤
  • Stability AI技术浅析(二):LDM
  • productionSourceMap:true -> 编译的时候不是那么乱码了
  • 详解 k 近邻(KNN)算法:原理、实践与调优 —— 以鸢尾花分类为例
  • C++面试——内存
  • docker重启或系统重启后harbor自动启动
  • MySQL快速恢复数据的N种方案完全教程
  • 口播数字人免费API调用方案
  • MC0439符号统计
  • 【学习笔记】NTP服务客户端配置
  • 9.对象介绍
  • 2025年COR SCI2区,泊位分配、岸桥分配与引航调度的集成规划,深度解析+性能实测
  • 下载及交叉编译zlib库,记录
  • 解决 MySQL 查询速度缓慢的问题