iOS 多线程技术基本原理

133 阅读7分钟

前言

多线程技术重要性就不必多说了,在技术开发届有着举足轻重的地位。同样,在iOS开发方面同样具有重要意义,接下来几个章节,作者会以 objective-c 语言为例,详细对多线程技术进行讲解。

多线程基本概念

线程

线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

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

进程

进程可以说是一个“执行中的程序”

  • 进程是指在系统中正在运行的一个应用程序。
  • 进程之间相互独立的。
  • 每个进程均运行在其专用的且受保护的内存空间内。 由下图可以到,在活动监视器中,一个应用程序代表一个进程

进程与线程之间的关系

  • 地址空间 :同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的 资源是独立的。
  • iOS开发中,每一个APP都是一个独立的进程,因此不会涉及到多进程相关问题。

多线程的意义

多线程并非真正意义上的多个任务并发执行。

  • 单核CPU)同一时间,CPU 只能处理1个线程。换言之,同一时间只有 1 个线程在执行。
  • 多线程同时执行:是 CPU 快速的在多个线程之间的切换,CPU 调度线程的时间足够快,就造成了多线程的“同时”执行的效果。如下图:
  • 如果线程数非常多:CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源,每个线程被调度的次数会降低,线程的执行效率降低。

多线程优点

  • 能适当提高程序的执行效率。
  • 能适当提高资源的利用率(CPU,内存)。

多线程缺点

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

线程的生命周期

线程的生命周期主要分为5部分,如下图所示:

  • 新建:主要是实例化线程对象

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

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

  • 阻塞:当满足某个预定条件时,可以使用休眠,即sleep,或者同步锁,阻塞线程执行。当进入sleep时,会重新将线程加入就绪中。下面关于休眠的时间设置,都是NSThread的

    • sleepUntilDate: 阻塞当前线程,直到指定的时间为止,即休眠到指定时间

    • sleepForTimeInterval: 在给定的时间间隔内休眠线程,即指定休眠时长

    • 同步锁:@synchronized(self):

  • 死亡:分为两种情况

    • 正常死亡,即线程执行完毕

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

线程池原理

在 GCD 多线程调用时,其实不需要我们手动创建线程,而是去线程池中获取一条可用线程,这样可以提升性能。具体的线程池查找原理如下图所示:

  • 【第一步】判断线程池中核心线程是否都正在执行任务
    • 返回NO,有空闲核心线程,安排空闲核心线程去执行任务
    • 返回YES,没有空闲线程进入【第二步】
  • 【第二步】判断线程池工作队列是否已经饱满
    • 返回NO,将任务添加到工作队列,等待CPU调度
    • 返回YES,进入【第三步】
  • 【第三步】判断线程池中的线程是否都处于执行状态
    • 返回NO,安排可调度线程池中空闲的线程去执行任务
    • 返回YES,进入【第四步】
  • 【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)
    • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行
    • CallerRunsPolicy:将任务回退到调用者
    • DisOldestPolicy:丢掉等待最久的任务
    • DisCardPolicy:直接丢弃任务

iOS中多线程的实现方案

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

线程安全问题

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

  • 互斥锁(即同步锁)

  • 自旋锁

线程间通讯

在Threading Programming Guide文档中,提及,线程间的通讯有以下几种方式:

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

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

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

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

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

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

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