iOS 多线程之 NSThread 的基本使用

多线程在需要执行耗时任务的时候非常用,大多时候我们不希望耗时任务阻塞主线程,因为处理用户界面的事件的相关操作都是放在主线程当中处理的,多线程也可以用来把复杂的任务分成多个比较小的任务,这样可以充分利用多核处理器的性能

基本概念

进程

进程(Process)是指正在执行的应用程序,具体来说进程是一个具有独立功能的程序关于某个数据集合进行的运行活动,包括申请和拥有系统资源,是一个活动的实体,每个进程之间是独立开来的,均运行在其专用且受保护的内存空间中

线程

macOS 中活动指示器中的进程和线程

线程是程序执行流的最小单元,是被系统独立调度和分派的基本单位(又称之为轻量级进程),由线程ID、程序计数器、寄存器集合和堆栈等内容组成,具体来说:

  • 进程(进行中的程序)需要执行任务必须存在线程,每个进程至少得有一个线程

  • 一个进程中所有任务都在线程中执行

线程的串行

准确的说一个线程中的任务执行是串行的。即一个线程中的多个任务只能挨个依次执行,同一时间一个线程只能执行一个任务。例如一个进程中开启一个线程下载多张图片,图片只能一张一张的按顺序下载

线程同步

线程同步简单来说就是多个线程协同步调在同一条线上按预定的先后次序运行,切不可理解为 “多个线程同时执行”,在 iOS 开发中常常使用到同步锁(synchronized)实现线程同步效果

在多线程编程中,一些敏感数据或共享数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多只有一个线程访问,以此保证数据安全

线程之间的通信

一个进程中如果开启了多个线程,某个线程可能需要和其它线程通信,以执行特定的任务,线程与线程间必定有一个信息传递的渠道,这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的,线程间的通信常涉及到以下几个问题:

  • 线程间如何传递信息
  • 线程之间如何同步,以使一个线程的活动不会破坏另一个线程的活动
  • 当线程间具有依赖关系时,如何调度多个线程的处理顺序
  • 共享数据如何保证安全,如何避免死锁问题等等

多线程数据安全问题

  • 线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据,当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

  • 线程安全:简单来说就是多个线程同时对共享资源进行访问时,采用了加锁机制,当一个线程访问共享资源,对该资源进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用

iOS 主线程

iOS 应用启动后,系统默认为应用开启一个线程称为主线程或 UI 线程,主线程的作用是处理UI事件(点击事件、滚动事件、拖拽事情),必须注意的是:刷新UI必须放在主线程中,不要把耗时操作(下载、大量计算)放在主线程执行,这会堵塞主线程,造成界面卡顿、影响用户体验

iOS 子线程

非UI线程、主线程之外的线程,iOS 开发中通常使用子线程处理耗时操作,例如:复杂计算、大量文件读取、文件下载等

NSThread

在 iOS 当中,苹果提供了三种主要的方式进行多线程编程:NSThread、Grand Central Dispatch (GCD) 和 NSOperation,本文主要讲解 NSThread 的基本使用,后续文章会讨论 GCD 和 NSOperation

NSThread 简介

NSThread 是经过 Apple 基于 pthread 封装的面向对象的多线程实现方案,它允许开发者直接以面向对象的思想对线程进行操作,每一个NSThread 对象就代表一条线程,但是开发者必须手动管理线程的生命周期

NSThread 的基本使用

使用 NSThread 创建线程的三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1.通过初始化方法创建线程,线程需要手动启动
// block 形式
let thread = Thread {
print("a new thread")
}
thread.start()
// target 形式
let thread = Thread(target: self, selector: #selector(run), object: nil)
thread.start()
// 3. 隐式的从主线程分离一个自动启动的线程
// block 形式 iOS 10 之后可用
Thread.detachNewThread {
print(Thread.current)
}
// target 形式
Thread.detachNewThreadSelector(#selector(run), toTarget: self, with: nil)
// 2.通过 performSelector 隐式的创建一个自动启动的线程
self.performSelector(inBackground: #selector(run), with: nil)

NSThread 的属性和相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//开启线程
open func start()
//取消线程
open func cancel()
//线程睡眠,阻塞线程
//线程睡眠到某个时间点
open class func sleep(until date: Date)
//线程睡眠给定的时间
open class func sleep(forTimeInterval ti: TimeInterval)
//退出线程,线程被销毁,不能再执行任务
open class func exit()
//判断当前线程是否为主线程
open class var isMainThread: Bool { get }
//获取主线程即UI线程
open class var main: Thread { get }
//获取当前线程
open class var current: Thread { get }
//线程的优先级,范围 0.0~1.0 iOS 8
open var threadPriority: Double
//线程的优先级,iOS8 之后可用,线程开启之后该属性为只读
open var qualityOfService: QualityOfService

线程间的通信

这里模拟一下开启新线程下载图片,然后回到主线程更下载的图片显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//开启新线程下载图片
self.performSelector(inBackground: #selector(download), with: nil)
}
func download() {
let url = URL.init(string: "https://www.baidu.com/img/bd_logo1.png")!
do {
let imageData = try Data.init(contentsOf: url)
let image = UIImage.init(data: imageData)
//方法一,把下载的图片传给主线程
self.performSelector(onMainThread: #selector(updateUI(image:)), with: image, waitUntilDone: true)
//方法二,把下载的图片传给主线程
self.perform(#selector(updateUI(image:)), on: Thread.main, with: image, waitUntilDone: true)
} catch let error as NSError {
print(error)
}
}
func updateUI(image:UIImage) {
//在主线程更新图片
self.imageView.image = image
}

线程同步

使用 NSThread 演示一个经典的线程安全的例子:多窗口卖票,假设一共有100张票,开启三个线程代表三个窗口同时卖票

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var thread1: Thread? //窗口1
var thread2: Thread? //窗口2
var thread3: Thread? //窗口3
//总票数100
var totalTicketCount = 100
override func viewDidLoad() {
super.viewDidLoad()
//创建线程
self.thread1 = Thread(target: self, selector: #selector(saleTicket), object: nil)
self.thread2 = Thread(target: self, selector: #selector(saleTicket), object: nil)
self.thread3 = Thread(target: self, selector: #selector(saleTicket), object: nil)
//设置线程名字
self.thread1?.name = "窗口1"
self.thread2?.name = "窗口2"
self.thread3?.name = "窗口3"
//开启线程
self.thread1?.start()
self.thread2?.start()
self.thread3?.start()
}
func saleTicket() {
//每个线程(窗口)都在这里循坏的卖票,直到票数为0终止
while true {
let currentCount = self.totalTicketCount
if currentCount > 0 {
self.totalTicketCount = currentCount - 1
print("\(String(describing: Thread.current.name!)) 卖了一张车票,还剩下\(self.totalTicketCount)张")
}else {
print("票卖完了")
break
}
}
}

打印结果如下:

由于多线程访问票数,所以票数数量出问题了,这个时候就需要保证票数操作的时候只能有一个线程访问,其他线程必须等待访问完才能访问,解决方案就是:同步锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func saleTicket() {
while true {
// objc_sync_enter 和 objc_sync_exit 成对使用,他们之间的代码就是需要线程同步的代码
// OC 使用 @synchronized 关键字
objc_sync_enter(self)
let currentCount = self.totalTicketCount
if currentCount > 0 {
self.totalTicketCount = currentCount - 1
print("\(String(describing: Thread.current.name!)) 卖了一张车票,还剩下\(self.totalTicketCount)张")
}else {
print("票卖完了")
break
}
objc_sync_exit(self)
}
}

同步锁除了使用上述关键字,还可以使用 NSLock,这里就不举例了

NSThread 需要自己管理线程的生命周期,线程同步,而且线程同步对数据的加锁会有一定的系统开销,实际开发中用的也比较少,苹果官方也更加推荐开发者使用 GCD 和 NSOperation

其他相关文章:

iOS多线程之GCD的基本使用

iOS多线程之 NSOperation 的基本使用

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!