十六:底层探索-多线程基础

320 阅读12分钟

平时开发中,多线程也与我们的开发紧密相关且被广泛应用,本篇章就对多线程的学习做个记录。

一:基础 - 线程 进程 队列

1.1 什么是进程

  • 进程是指在系统中正在运行的一个应用程序,手机上的任何一个App都是一个进程。
  • 每个进程之间是独立的,每个进程均运行在齐专用的且受保护的内存,一个进程的崩溃并不会影响到其他的进程。
  • 通过“活动监视器”可以查看 Mac 系统中所开启的进程。

1.2 什么是线程

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

1.3 进程与线程的区别

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

1.4 线程与RunLoop

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

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

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

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

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

1.5 延伸:内存五大区域

栈区、堆区、全局区、常量区、代码区

image.png

栈区(stack)

  • 特点

    • 栈是系统数据结构,其对应的进程或者线程是唯一的
    • 栈是向低地址扩展的数据结构
    • 栈是一块连续的内存区域,遵循先进后出(FILO)原则
    • 栈的地址空间在iOS中是以0X7开头
    • 栈区一般在运行时分配
  • 存储内容

    • 栈区是由编译器自动分配并释放的,主要用来存储局部变量
    • 函数的参数,例如函数的隐藏参数(id selfSEL _cmd
  • 优缺点

    • 优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效
    • 缺点:栈的内存大小有限制,数据不灵活
    • iOS主线程栈大小是1MB,其他主线程是512KBMAC只有8M

传入函数的参数值、函数体内声明的局部变量等,由编译器自动分配释放,通常在函数执行结束后就释放了。(注意:不包括static修饰的变量,static意味该变量存放在全局/静态区)

  • 堆区(heap)

    • 特点

      • 堆是向高地址扩展的数据结构
      • 堆是不连续的内存区域,类似于链表结构(便于增删,不便于查询),遵循先进先出(FIFO)原则
      • 堆的地址空间在iOS中是以0x6开头,其空间的分配总是动态的
      • 堆区的分配一般是在运行时分配
    • 存储内容

      • 堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收
      • OC中使用alloc或者使用new开辟空间创建对象
      • C语言中使用malloccallocrealloc分配的空间,需要free释放
    • 优缺点

      • 优点:灵活方便,数据适应面广泛
      • 缺点:需手动管理,速度慢、容易产生内存碎片

    当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区。因为现在iOS基本都使用ARC来管理对象,所以也不需要手动释放。

  • 全局区(静态区)(BSS段)

    • BSS段bss segment)通常是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配

    • 数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域,数据段属于静态内存分配。

    • 全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放

      • 未初始化的全局变量和静态变量,即BSS区(.bss
      • 已初始化的全局变量和静态变量,即数据区.data
    • static修饰的变量会成为静态变量,该变量的内存由全局/静态区在编译阶段完成分配,且仅分配一次。

    • static可以修饰局部变量也可以修饰全局变量。

  • 常量区(数据段)

    • 常量区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序结束后由系统释放
    • 通常是指用来存放程序中已经初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。字符串常量等,是放在只读数据段中,结束程序时才会被收回。
  • 代码区(代码段)

    • 代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
    • 代码区需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。

二:多线程

2.1 多线程意义

优点

  • 能适当提⾼程序的执⾏效率。
  • 能适当提⾼资源的利⽤率(CPU,内存)。
  • 线程上的任务执⾏完成后,线程会⾃动销毁。

缺点

  • 开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占 512 KB)。
  • 如果开启⼤量的线程,会占⽤⼤量的内存空间,降低程序的性能。
  • 线程越多,CPU 在调⽤线程上的开销就越⼤。
  • 程序设计更加复杂,⽐如线程间的通信、多线程的数据共享。

2.2 多线程原理

时间⽚的概念:CPU在多个任务直接进⾏快速的切换,这个时间间隔就是时间⽚

  • 单核CPU)同⼀时间,CPU 只能处理 1 个线程,换⾔之,同⼀时间只有 1 个线程在执⾏
  • 多线程同时执⾏其实是 CPU 快速的在多个线程之间的切换,CPU 调度线程的时间⾜够快,就造成了多线程的同时执⾏的效果。但是如果线程数⾮常多,CPU 会在 N 个线程之间切换,消耗⼤量的 CPU 资源,每个线程被调度的次数会降低,线程的执⾏效率降低。

image.png

2.3 线程的生命周期

image.png

  • 新建:实例化线程对象

  • 就绪:向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。

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

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

  • 死亡:正常死亡,线程执行完毕。非正常死亡,当满足某个条件后,在线程内部中止执行/在主线程中止线程对象。

线程池

线程池(英语:thread pool):一种线程使用模式。 线程过多会带来调度开销,进而影响缓存局部性和整体性能。 而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。 这避免了在处理短时间任务时创建与销毁线程的代价。

未命名文件.png

  • 如果都在执行状态,则交给饱和策略处理
  • 如果饱满,则会判断线程是否都处于执行状态,如果没有,则安排非核心线程去执行
  • 如果都在执行,就会检查工作队列是否饱满,如果未饱满,就会将任务存储在工作队列
  • 有新任务来时,会先判断线程池是否都在执行任务,如果没到就会创建线程执行任务

饱和策略有四种模式:

  1. DisCardPolicy:直接丢弃任务
  2. DisOldestPolicy:丢掉等待最久的任务
  3. CallerRunsPolicy:将任务回退到调用者
  4. AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行

2.4 多线程方案

技术方案简介语言线程生命周期使用评率
pthread一套通用的多线程API,适用于Unix/Linux/Windows等系统,跨平台/可移植,使用难度大C程序员管理几乎不用
NSThread使用更加面向对象,简单易用,可直接操作线程对象OC程序员管理偶尔使用
GCD旨在替代NSThread等线程技术,充分利用设备的多核C自动管理经常使用
NSOperation基于GCD(底层是GCD),比GCD多了一些更简单实用的功能,使用更加面向对象OC自动管理经常使用

2.5 线程间通讯

  • 直接消息传递: 通过performSelector的一系列方法,可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化

  • 全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。 否则可能会导致竞争状况,数据损坏或崩溃。

  • 条件执行: 条件是一种同步工具,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。

  • Runloop sources: 一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息。由于 Runloop source 是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率

  • Ports and sockets:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态

  • 消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效

  • Cocoa 分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高

2.6 线程安全

在使用多线程时,往往会出现资源抢夺问题,这时就需要使用线程同步技术来解决线程安全的问题,所谓的线程同步:即指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

线程同步技术方案有很多,其中之一就是枷锁,大家可以看看这篇文章不再安全的 OSSpinLock

线程安全之后的篇章还会做一些详细介绍,在这就不展开说了。

关于atomic和nonatomic这里延伸一下:
atomic    

  • 是原子属性,是为多线程开发准备的,是默认属性!
  • 仅仅在属性的 setter 方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行操作
  • 同一时间 单(线程)写多(线程)读的线程处理技术

nonatomic 

  • 是非原子属性
  • 没有锁!性能高!

参考:
多线程系列文章
iOS多线程