基础知识
-
系统中正在运行的每一个应用程序都是一个 进程(Process) ,每个进程系统都会分配给它独立的内存运行。也就是说,在iOS系统中中,每一个应用都是一个进程。
-
一个进程的所有任务都在 线程(Thread) 中进行,因此每个进程至少要有一个线程,也就是主线程。那多线程其实就是一个进程开启多条线程,让所有任务并发执行。
-
任务、线程、队列:
任务是执行的代码块(可以是一个简单函数或者是一个复杂计算)
线程是执行代码的基本单位。每个应用程序都有主线程(UI线程)和可能的多个工作线程。
线程可以执行任务,通过调度来管理线程的执行。
队列是管理任务执行的结构。它们按照特定的顺序(先进先出)来处理任务。队列分为串行队列(一次只执行一个任务,任务按添加顺序依次执行)和并行队列(可以同时执行多个任务)。
总的来说:任务是具体的工作,队列是管理任务执行的容器,而线程是执行这些任务的环境。通过合理使用队列,您可以更高效地管理任务和线程,提高应用的性能和响应性。
-
iOS App一旦运行,默认就会开启一条线程。这条线程,通常称作为“主线程”。在iOS应用中主线程的作用一般是:
刷新UI;
处理UI事件,例如点击、滚动、拖拽。 -
如果主线程的操作太多、太耗时,就会造成App卡顿现象严重。所以,通常我们都会把耗时的操作放在子线程中进行,获取到结果之后,回到主线程去刷新UI。
-
多线程在一定意义上实现了进程内的资源共享,以及效率的提升。同时,在一定程度上相对独立,它是程序执行流的最小单元,是进程中的一个实体,是执行程序最基本的单元,有自己栈和寄存器。
-
线程的生命周期:
新建:线程已创建,但尚未启动。
可运行:线程已准备好运行并等待 CPU 的时间。
正在运行:线程当前正在执行。
阻塞:线程正在等待外部事件(例如 I/O)。
终止:线程已完成执行。
- 并行 :真正同时执行多个任务,通常在多核处理器上实现。
- 并发 :多个任务在同一时间段内交替执行,但不一定是真正的同时。即使在单核处理器上,也可以通过快速切换实现并发。
串行和并行描述的是任务执行的方式,而并发则是任务调度的概念,强调的是任务之间的相互独立性。
通过确保主线程自由响应用户事件,并发可以很好地提高应用的响应性。通过将工作分配到多核,还能提高应用处理的性能。但是并发也带来一定的额外开销(调度),并且使代码更加复杂,更难编写和调试。
多线程
同一时间,CPU只能处理一条线程,也就是只有一条线程在工作。所谓多线程并发(同时)执行,其实是CPU快速的在多线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
优点:
- 能适当提高程序的执行效率
- 能适当提高资源的利用率,这个利用率表现在(CPU,内存的利用率)
缺点:
- 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB,如果开启大量的线程,会占用大量的内存空间,降低程序的性能)
- 线程越多,CPU在调度线程上的开销就越大
- 线程越多,程序设计就越复杂,比如线程之间的通信,多线程的数据共享,这些都需要程序的处理,增加了程序的复杂度。
注意:
- 别将比较耗时的操作放在主线程中
- 耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
目前iOS多线程有四种方法:pthread,NSThread,GCD, NSOperation,以下为对比图:
pthread就不介绍了,以下将从多角度介绍NSThread、GCD、NSOperation。
NSThread
NSThread是 Apple 官方提供面向对象操作线程的技术。可以直接操作线程对象,缺点是需要自己手动控制线程的生命周期,这会增加开发难度和内存泄漏风险,此外,NSThread因为没有直接提供像GCD 以及NSOperation 提供task 依赖关系管理功能,所以不易管理复杂的任务依赖和顺序。
但仍可透过NSCondition 来管理任务依赖(NSCondition
是一个用于线程同步的工具,允许一个线程在满足特定条件之前暂停执行,并在其他线程通知它时继续运行。它通常用于实现复杂的线程间通信。)
GCD
GCD(Grand Central Dispatch)是 Apple 提供的一种用于 多核并行计算 的 API,旨在简化并发编程,提升应用的性能。它通过管理系统线程池,让开发者专注于任务本身,而不是复杂的线程管理。GCD 通过(FIFO)队列来管理任务的调度,自动处理底层的线程管理,使得你可以专注于任务的逻辑,而不必处理线程的创建、调度和回收。如下图为GCD的一种形式:
简单来说就是:GCD 是一个帮助你自动管理线程和任务队列的工具,你只需告诉它要做什么任务,它就会帮你高效地安排执行。
任务(Task)
任务就是你想要执行的具体操作,比如某段代码或函数。任务可以同步(sync)或异步(async)执行。
- 同步(sync):调用后等待任务执行完毕,当前线程会被阻塞,直到任务完成。
- 异步(async):任务执行后不需要等待结果,当前线程继续执行后续代码,不会阻塞。
队列(Queue)
GCD 提供了 任务队列 来管理任务的调度。队列是一种 FIFO(先进先出) 数据结构,也就是任务按顺序入队和出队。
-
串行队列(Serial Queue):一次只执行一个任务,任务按照入队的顺序依次执行。常用于确保任务按顺序进行,避免并发冲突。
注意:串行队列中执行任务不允许被当前队列中的任务阻塞(此时会死锁),但可以被别的队列任务阻塞。(就是任务A可以等待其它队列的任务同步完成,但是不能等待自己所在的串行队列中的任务同步完成,在不同队列中同步调用则不会导致死锁,因为它们是独立的队列,当前任务可以等待另一个队列中的任务完成,而不会影响自己队列的任务调度。)
-
并发队列(Concurrent Queue):允许多个任务并发执行,但依然按照入队的顺序开始。并发队列可以同时处理多个任务,但系统决定任务在什么时候以及在哪个线程上运行。
主队列
主队列是一个特殊的串行队列,所有在主线程上执行的任务都在这个队列中运行。UI 更新等必须在主线程上进行的操作,通常使用主队列。
系统全局并行队列
全局并发队列:存在5个不同的QoS级别,可以使用默认优先级,也可以单独指定,全局队列底层由数组创建,平时使用网络请求(例如第三方包Alamofire)都是对全局并行队列进行了一个封装,所以看不到直接使用的代码。
QoS优先级:
userInteractive > userInitiated > default > utility > background
tips:系统自动帮我们创建的6 条队列:1 条系统主队列(串行),5 条全局并发队列(不同优先级),它们是我们创建的所有队列的最终目标队列,这 6 个队列负责所有队列的线程调度。
队列任务
同步执行任务(sync)
- 必须等待当前任务(代码块)执行完毕,才会执行下一条任务
- 任务一经提交就会阻塞当前队列(若是并发队列,理解为阻塞当前行),并请求队列立即安排执行该同步任务(即线程切换到另一个队列执行,执行完毕回到当前队列
- 不具备开启多线程的能力
1.在主队列中存在一个手动创建的串行队列serialQueue,serialQueue队列同步执行任务:
//手动创建串行队列 + 同步执行任务
let serialQueue = DispatchQueue(label: ".com1")serialQueue.sync {print("同步1",Thread.current)
}
print("同步2",Thread.current)
输出:
同步1 <_NSMainThread: 0x600001704080>{number = 1, name = main}
同步2 <_NSMainThread: 0x600001704080>{number = 1, name = main}
以上可知,原本serialQueue队列中没有任务执行,此时提交一个同步任务到serialQueue队列中,因为是同步任务,主线程阻塞,在serialQueue队列中执行同步任务,同步任务执行完毕后返回到主线程执行同步2。由于同步1、同步2均在主线程中执行的(<_NSMainThread: 0x600001704080>{number = 1, name = main}),我们可以知道没有开辟新的线程,只是线程通过队列调度(在两个队列中切换)来执行任务。
2.在主线程中同步执行任务:
//主线程(特殊的串行队列) + 同步执行任务
DispatchQueue.main.sync {print("同步",Thread.current)
}
报错:
这是因为发生了死锁,原因是主线程是特殊的串行队列,除了执行开发者主动提交的任务,还负责 UI 更新和系统任务。也就是说,在你提交任务之前,主线程(串行队列)中就已经有负责 UI 更新的任务和系统任务了,如果你同步执行任务,并且因为串行队列是依次执行的,这时候就会造成死锁。(第一个手动创建的串行队列没有造成死锁是因为在你同步执行任务之前,serialQueue队列中没有需要执行的任务)
所以我们也可以了解到serialQueue队列的死锁:
//串行队列 + 同步执行
let serialQueue = DispatchQueue(label: ".com1")serialQueue.async {print("同步2",Thread.current)serialQueue.sync {print("同步1",Thread.current)}
}
结果是输出:
同步2 <NSThread: 0x600001748b80>{number = 5, name = (null)}
后就报错了(死锁)。
综上所述,我们可以知道以下几点:
- 串行队列中的任务是依顺序执行的
- 同步执行任务会阻塞当前线程,即立即执行同步任务
- 主线程是特殊的串行队列,在主线程中时刻有更新UI和系统的任务在执行
- 判断串行队列+同步执行任务是否会死锁的关键在于串行队列在加入同步任务之前是否有待执行的任务
- 线程通过队列调度(在两个队列中切换)来执行任务的
3.在并行队列中执行同步任务:
//并行队列 + 同步执行
let concurrentQueue = DispatchQueue(label: ".com2",attributes: .concurrent)concurrentQueue.sync {print("同步1",Thread.current)concurrentQueue.sync {print("同步2",Thread.current)}print("同步3",Thread.current)
}
输出:
同步1 <_NSMainThread: 0x600001704040>{number = 1, name = main}
同步2 <_NSMainThread: 0x600001704040>{number = 1, name = main}
同步3 <_NSMainThread: 0x600001704040>{number = 1, name = main}
以上可知,并行队列在执行任务的时候,同步任务加入其中,线程会转去执行该同步任务,同步任务执行结束后再回到原任务。所以可以把并行队列看成很多个串行队列组成的队列,加入同步任务时,线程离开正在执行的一行转去执行同步任务的那一行,此时同样没有创建新的线程,所以一直都是并行队列调度一个线程执行不同行的任务。
综上所述,我们可以知道以下几点:
- 并行队列看成很多个串行队列组成的队列
- 加入同步任务时,线程离开正在执行的一行转去执行同步任务的那一行
- 在 GCD 中,是队列调度线程,任务被添加到一个队列中,由系统根据队列的类型(串行队列或并行队列)来调度任务。当一个任务被放到队列中,系统会选择适当的线程来执行该任务。如果是串行队列,任务会一个接一个地在同一个线程中执行;如果是并行队列,系统可能会选择不同的线程同时执行多个任务。
异步执行任务(async)
- 不用等待当前任务执行完毕,就可以执行下一条任务
- 具备开启多线程的能力
- 特性:任务提交后不会阻塞当前队列,会由队列安排另一个线程执行
1.串行队列中执行异步任务
//串行队列 + 同步执行
let serialQueue = DispatchQueue(label: ".com3")
serialQueue.async {print("异步1", Thread.current)}
输出:
异步1 <NSThread: 0x60000174b500>{number = 5, name = (null)}
2.并行队列中执行异步任务
//并发队列 + 同步执行
let concurrentQueue = DispatchQueue(label: ".com4",attributes: .concurrent)
concurrentQueue.async {print("异步1", Thread.current)}
输出:
异步1 <NSThread: 0x600001700e40>{number = 6, name = (null)}
可以看出,异步执行任务具备开启新线程的能力
3.在串行队列中执行同步任务,再执行异步任务
//串行队列 + 异步执行
let serialQueue = DispatchQueue(label: ".com5")
serialQueue.sync {print("同步1", Thread.current)serialQueue.async {print("异步1", Thread.current)}print("同步2", Thread.current)
}
print("同步3", Thread.current)
输出:
同步1 <_NSMainThread: 0x60000170c000>{number = 1, name = main}
同步2 <_NSMainThread: 0x60000170c000>{number = 1, name = main}
同步3 <_NSMainThread: 0x60000170c000>{number = 1, name = main}
异步1 <NSThread: 0x600001750000>{number = 5, name = (null)}
在主线程中手动创建串行队列serialQueue,将同步任务加入到serialQueue队列,此时阻塞主线程,然后将异步任务加入到serialQueue队列中,因为是串行队列(serialQueue、主线程也是),所以异步任务会拍到队列队尾。
4.在主线程中存在两个手动创建serialQueue、serialQueue2串行队列
//串行队列 + 异步执行
let serialQueue = DispatchQueue(label: ".com5")
serialQueue.sync {print("同步1", Thread.current)serialQueue.async {print("异步1", Thread.current)}print("同步2", Thread.current)
}
print("同步3", Thread.current)
let serialQueue2 = DispatchQueue(label: ".com6")
serialQueue2.async {print("异步2", Thread.current)
}
输出:
同步1 <_NSMainThread: 0x600001700000>{number = 1, name = main}
同步2 <_NSMainThread: 0x600001700000>{number = 1, name = main}
同步3 <_NSMainThread: 0x600001700000>{number = 1, name = main}
异步1 <NSThread: 0x6000017480c0>{number = 3, name = (null)}
异步2 <NSThread: 0x6000017472c0>{number = 4, name = (null)}
为什么异步1在异步2前面呢?是因为在主线程中任务是按顺序执行的。
这里多加一个样例,是想表明,以上线程的前提都是在主线程中创建的,首先要满足主线程(特殊的串行队列)这一前提条件!!!!
总结:
注意:同步异步的区别在于是否会阻塞当前线程,而不是是否会开启新的线程!
栅栏任务(barrier)
栅栏任务的主要特性是可以对队列中的任务进行阻隔,执行栅栏任务时,它会先等待队列中已有的任务全部执行完成,然后它再执行,在它之后加入的任务也必须等栅栏任务执行完后才能执行。
这个特性更适合并行队列,而且对栅栏任务使用同步或异步方法效果都相同。
let concurrentQueue = DispatchQueue(label: "com.zhalan", attributes: .concurrent)let task = DispatchWorkItem(flags: .barrier, block: {print(Thread.current)
})concurrentQueue.sync(execute: task)
concurrentQueue.async(execute: task)
输出:
<_NSMainThread: 0x600001700000>{number = 1, name = main}
<NSThread: 0x60000174dd80>{number = 5, name = (null)}
我们模拟一下栅栏任务的使用环境,先在并行队列中加入三个异步任务,然后加入栅栏任务,然后再加入三个异步任务。
let concurrentQueue = DispatchQueue(label: "com.zhalan", attributes: .concurrent)let task = DispatchWorkItem(flags: .barrier, block: {print("栅栏",Thread.current)
})concurrentQueue.async {print("异步1",Thread.current)
}
concurrentQueue.async {print("异步2",Thread.current)
}
concurrentQueue.async {print("异步3",Thread.current)
}concurrentQueue.sync(execute: task)concurrentQueue.async {print("异步4",Thread.current)
}
concurrentQueue.async {print("异步5",Thread.current)
}
concurrentQueue.async {print("异步6",Thread.current)
}
输出:
异步2 <NSThread: 0x600001758900>{number = 6, name = (null)}
异步1 <NSThread: 0x6000017580c0>{number = 4, name = (null)}
异步3 <NSThread: 0x600001721c40>{number = 7, name = (null)}
栅栏 <_NSMainThread: 0x600001708000>{number = 1, name = main}
异步4 <NSThread: 0x600001721c40>{number = 7, name = (null)}
异步5 <NSThread: 0x6000017580c0>{number = 4, name = (null)}
异步6 <NSThread: 0x600001758900>{number = 6, name = (null)}
异步任务输出顺序不一样,可以看到是符合栅栏任务的特性的。
迭代任务
并行队列利用多个线程执行任务,可以提高程序执行的效率。而迭代任务可以更高效地利用多核性能,它可以利用 CPU 当前所有可用线程进行计算(任务小也可能只用一个线程)。如果一个任务可以分解为多个相似但独立的子任务,那么迭代任务是提高性能最适合的选择。
延时任务(asyncAfter)
延迟任务的执行
let concurrentQueue = DispatchQueue(label: "com.zhalan", attributes: .concurrent)let task = DispatchWorkItem(block: {print("延时1",Thread.current)
})concurrentQueue.asyncAfter(deadline: .now() + 0.2, execute: task)concurrentQueue.async {print("异步1",Thread.current)
}
输出:
异步1 <NSThread: 0x600001714e80>{number = 7, name = (null)}
延时1 <NSThread: 0x600001747580>{number = 6, name = (null)}
任务组(DispatchGroup)
DispatchGroup 可以将多个任务组合在一起并且监听它们的完成状态。当所有任务都完成时,可以通过通知回调或等待的方式知道它们的执行结果。
let group = DispatchGroup()
let queue = DispatchQueue(label: ".com1",attributes: .concurrent)queue.async(group: group) {print("任务1完成")
}queue.async(group: group) {print("任务2完成")
}// 当所有任务完成时通知
group.notify(queue: DispatchQueue.main) {print("所有任务完成")
}// 或者阻塞等待所有任务完成
group.wait()
print("所有任务完成(等待方式)")
输出:
任务2完成
任务1完成
所有任务完成(等待方式)
所有任务完成
以上代码样例使用任务组完成两个异步任务,并且阻塞等待所有任务完成,当所有任务完成时通知。
除了
queue.async(group: group) {
}
这种使用方法之外,还有一种方法,通过group.enter()、group.leave():
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.1")group.enter()
queue1.async(){for i in 0...10{print("i = \(i)",Thread.current)}group.leave()
}
信号量(DispatchSemaphore)
DispatchSemaphore 用于控制并发任务的执行。通过信号量可以确保一次只有有限数量的任务可以执行(它可以通过计数来标识一个信号),常用于资源访问的控制。
例如,控制同一时间写文件的任务数量、控制端口访问数量、控制下载任务数量等。
信号量的使用非常简单:
• semaphore.wait() :使用 wait
方法让信号量减 1,再安排任务。如果此时信号量仍大于或等于 0,则任务可执行,如果信号量小于 0,则任务需要等待其他地方释放信号。
• semaphore.signal() 会增加信号量。
样例:
//设置信号量为2 -----即最大可允许执行任务的数量为2
let semaphore = DispatchSemaphore(value: 2)let queue = DispatchQueue(label: ".com1",attributes: .concurrent)semaphore.wait()
queue.async {print("任务1完成")semaphore.signal()
}semaphore.wait()
queue.async {print("任务2完成")semaphore.signal()
}semaphore.wait()
queue.async {print("任务3完成")semaphore.signal()
}
输出:
任务1完成
任务2完成
任务3完成
没有信号量的话,任务1、2、3是异步执行的,输出顺序无法确定,但是使用信号量后,执行任务1、2的时候,信号量为0,任务3必须等待任务1或者任务2释放信号量后,才可以执行。
任务组和信号量区别:
• DispatchGroup 用于监听多个任务的完成状态,适合任务同步和协调。
• DispatchSemaphore 用于控制任务的并发数,常用于限制资源访问或保护临界区。
NSOperation && NSOperationQueue
NSOperation
- NSOperation 是一个抽象类,用于封装任务。
- 常用子类:BlockOperation(封装多个代码块)。
- 支持任务取消 (cancel())、依赖 (addDependency())、和任务完成回调 (completionBlock)。
let operation = BlockOperation {print("任务1执行中")
}
operation.start() // 启动任务
可以把NSOperation看成任务Task
NSOperationQueue
- NSOperationQueue 管理NSOperation任务执行,支持并发控制和依赖。
- 默认并行队列,设置 maxConcurrentOperationCount 控制并发数。
- 一般是并发执行
// 创建一个 NSOperationQueue 实例,负责管理任务执行
let operationQueue = OperationQueue()// 定义第一个任务,使用 BlockOperation 执行一个简单的打印操作
let task1 = BlockOperation {print("任务1开始")sleep(2) // 模拟任务执行需要2秒时间print("任务1结束")
}// 定义第二个任务,同样使用 BlockOperation 来执行打印操作
let task2 = BlockOperation {print("任务2开始")sleep(1) // 模拟任务执行需要1秒时间print("任务2结束")
}// 设置依赖关系:任务2需要等待任务1完成后才能执行
task2.addDependency(task1)// 将两个任务添加到队列中,任务会按照依赖关系自动执行
operationQueue.addOperation(task1)
operationQueue.addOperation(task2)// 这样 task1 会先执行完,再执行 task2
多线程并发打开了一扇新的大门,可以让开发者根据自己的理解去优化项目。
跟着思路理解,沉下心敲一敲,相信我,你会有很大的收获。
参考:
https://medium.com/@hooy123456_58230/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-ios-%E5%A4%9A%E7%B7%9A%E7%A8%8B%E8%99%95%E7%90%86-gcd-nsthread-%E8%88%87-nsoperation-%E6%AF%94%E8%BC%83-488bd54e309e
GCD:异步同步?串行并发?一文轻松拿捏!_gcd异步串行-CSDN博客
iOS多线程总结(GCD、NSOperation、NSTread) | 跃迁引擎
GitHub - leejtom/multiThreading: iOS多线程(Pthread、NSThread、GCD、NSOperation)