iOS

iOS 底层原理 - 多线程一

2020-08-14  本文已影响0人  yan0_0

线程的定义

• 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
• 进程要想执行任务,必须得有线程,进程至少要有一条线程
• 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

进程的定义

进程是指在系统中正在运行的一个应用程序
每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存
iOS是单进程,安卓是多进程,因为iOS的沙盒机制,单进程会更加安全,进程切换时,消耗的资源大

进程和线程的关系

• 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
• 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间
的资源是独立的。
• 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程 都死掉。所以多进程要比多线程健壮。
• 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。 同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
• 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线 程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
• 线程是处理器调度的基本单位,但是进程不是。

线程和队列的关系

队列是一种数据结构,用来调度任务,线程相当于收银员,队列相当于排的队伍。

任务执行的速度

任务复杂度、CPU、线程数量、任务的优先级、队列的情况等有关系。

代码五大分区结构

堆,栈,静态区,常量区,代码区
1.栈:存放系统临时创建的局部变量,传入函数的参数值、函数体内声明的局部变量等,由编译器自动分配释放,通常在函数执行结束后就释放了。(注意:不包括static修饰的变量,static意味该变量存放在全局/静态区)
其操作方式类似数据结构中的栈,先进后出。
2.堆:堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或 缩减。变量通过new、alloc、malloc、realloc分配的内存块就存放在堆区。堆区的内存都是动态分配的。
3.静态区 :全局/静态区是存放全局变量和静态变量的。也就是bss段。
4.常量区:常量区是一块比较特殊的存储区,常量区里面存放的是常量,常量字符串就存放在常量区。
常量区的内存在编译阶段完成分配,程序运行时会一直存在内存中,只有当程序结束后才会由操作系统释放。
5.代码区:代码区是用来存放可执行文件的操作指令(存放函数的二进制代码),其实就是存放程序的所有代码。代码区需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。

堆区地方大,一般使用指针方式去指,指针在栈里,达到优化内存的效果。假如有大量临时变量在栈区时候,我们就需要用@autorelease来释放临时变量。
野指针的产生:
当一个对象被创建时候,会有一个指针指向这个对象,并且通过修改链表空间表示该块内存已被使用,但是当对象被释放了时候,如果该指针还指向这块内存,这时候就产生了一个经典问题: 野指针。

野指针,无法被其他人使用,无法释放,使用第三方工具静态分析检测野指针

多线程的意义

多线程技术方案

屏幕快照 2020-08-12 上午1.04.34.png

多线程原理

CPU在单位时间片里快速在各个线程之间切换

多线程生命周期

多线程生命周期包含五个阶段:新建,就绪,运行,死亡,阻塞。
新建:就是刚使用new方法,new出来的线程。
就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行。
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能。
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态。
死亡:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。


屏幕快照 2020-08-12 上午12.39.26.png

注意线程start不一定是在运行

线程池

线程池是多线程处理的一种形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池中的线程都是后台线程。每个线程都有默认的堆栈大小,以默认的优先级运行,并处在多线程单元中。顾名思义就是一个管理多个线程生命周期的池子。iOS开发中不会直接接触到线程池,这是因为GCD已经包含了线程池的管理,我们只需要通过GCD获取线程来执行任务即可。

可调度线程池

屏幕快照 2020-08-12 上午12.51.10.png

饱和策略

饱和策略分为四种状态
• AbortPolicy 直接抛出RejectedExecutionExeception 异常来阻止系统正常运行
• CallerRunsPolicy 将任务回退到调用者
• DisOldestPolicy 丢掉等待最久的任务‘
• DisCardPolicy 直接丢弃任务
• 这四种拒绝策略均实现的RejectedExecutionHandler接口

线程和runloop的关系

1:runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的, 是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字 典里。
2:runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠 状态,有了任务就会被唤醒去执行任务。
3:runloop在第一次获取时被创建,在线程结束时被销毁。
4:对于主线程来说,runloop在程序一启动就默认创建好了。
5:对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线 程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。

互斥锁小结

保证锁内的代码,同一时间,只有一条线程能够执行
互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差

automic与nonatomic的区别

1.nonatomic非原子性属性
2.atomic 原子属性(线程安全),针对多线程设计的,默认值
保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
atomic 本身就有一把锁(自旋锁)
单写多读:单个线程写入,多个线程可以读取

3.atomic:线程安全,需要消耗大量的资源
4.nonatomic:非线程安全,适合内存小的移动设备
iOS 开发的建议
• 所有属性都声明为 nonatomic
• 尽量避免多线程抢夺同一块资源
• 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

C和OC的桥接

__bridge只做类型转换,但是不修改对象(内存管理权)
__bridge_retain(也可以使用CFBridgingRetain)将OC的对象转换成的Core Foundation的对象,同时也犟对象(内存)的管理权交给我们,后续需要使用CFRelease或者相关方法来释放对象
__bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换成Objective-C的对象,同时将对象(内存)的管理权交给ARC

线程间的通讯

1.几种线程间的通讯方式
苹果的官方文档给我们列出了线程间通讯的几种方式:

截屏2020-09-08 下午7.18.56.png

上图的表格是按照技术复杂度由低到高顺序排列的,其中后两种只能在OS X中使用。
直接消息传递: 通过 -performSelector: 的一系列方法(例如performSelector:onThread),可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化。
全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。 否则可能会导致竞争状况,数据损坏或崩溃。
条件执行: 一种特殊的锁--条件锁,当使用条件锁使一个线程等待(wait)时,该线程会被阻塞并进入休眠状态,在另一个线程中对同一个条件锁发送信号(single),则等待中的线程会被唤醒继续执行任务
Runloop sources: 一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息。由于 Runloop source 是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率。
Ports and sockets: 基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态。
消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效。
Cocoa 分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高

稍微总结一下NSPort的使用要点:

NSPort对象必须添加到要接收消息的线程的Runloop中,必须由Runloop来进行管理
接收消息的对象实现NSPortDelegate协议的-handlePortMessage:方法来获取消息内容

上一篇下一篇

猜你喜欢

热点阅读