前言
多线程技术重要性就不必多说了,在技术开发届有着举足轻重的地位。同样,在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 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高