NSOperation
上一篇文章记录了GCD的使用,各种用法都有提到了,现在再写一篇笔记记录一下NSOperation的使用!
首先附上别人制作的一副图片:
网上的资料很多,细节的东西百度即可得,此文重点以简单的代码作为笔记。
1、子类 BlockOperation
(1)单独使用BlockOperaton
代码:
// MARK: - BlockOperation
func blockOperation() {
let op = BlockOperation {
print(Thread.current)
for i in 0...2 {
print("\(i) -> **************")
// 模拟耗时
Thread.sleep(forTimeInterval: 1)
}
}
op.start()
}
结果:
划重点:
- 仅使用BlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
- 代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。
(2)addExecutionBlock
在(1)的基础上,添加addExecutionBlock操作。
代码:
// MARK: - BlockOperation + addExecutionBlock
func blockOperation() {
let op = BlockOperation {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
op.addExecutionBlock {
for i in 0...2 {
print("\(i) -> +++++++++++++++ ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
op.addExecutionBlock {
for i in 0...2 {
print("\(i) -> --------------- ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
op.addExecutionBlock {
for i in 0...2 {
print("\(i) -> >>>>>>>>>>>>>>> ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
op.start()
}
结果:
划重点:
BlockOperation
初始化时的block操作和addExecutionBlock
的block操作是在不同的线程执行的。虽说前者仍然是在主线程调用,且打印线程信息也为主线程,但是根据其它资料显示,初始化时的block线程并不确定。一般情况下,如果一个
BlockOperation
对象封装了多个操作。BlockOperation
是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。
2、创建队列
- 获取主队列
// swift获取主队列
let queue: OperationQueue = OperationQueue.main
// OC获取主队了
NSOperationQueue *queue = [NSOperationQueue mainQueue];
- 创建自定义队列
// swift创建自定义队列
let queue: OperationQueue = OperationQueue()
// OC创建自定义队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
3、添加任务到队列
(1)主队列添加任务
代码:
// MARK: - 主队列添加任务
func mainQueue() {
let queue: OperationQueue = OperationQueue.main
queue.addOperation {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
queue.addOperation {
for i in 0...2 {
print("\(i) -> >>>>>>>>>>>>>>> ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
}
结果:
划重点:
主队列中添加任务,所有任务都会在主线程中执行,且为同步串行执行。
(2)添加任务到自定义队列
代码:
// MARK: - 添加任务到自定义队列
func addTaskToQueue() {
let queue: OperationQueue = OperationQueue()
queue.addOperation {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
queue.addOperation {
for i in 0...2 {
print("\(i) -> >>>>>>>>>>>>>>> ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
// 另一种添加任务的写法
let op = BlockOperation {
for i in 0...2 {
print("\(i) -> +++++++++++++++ ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
queue.addOperation(op)
}
结果:
划重点:
添加到自定义队列中的操作,会自动在子线程中执行。
本例默认为并发执行,也可串行执行
4、串行、并发控制
上个例子中我们看到了自定义队列添加任务时的并发执行。那么如何控制串行执行呢?
这时我们就要用到队列OperationQueue
的一个属性maxConcurrentOperationCount
。
提前划重点:
maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。
- maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
- maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
- maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min(自己设定的值,系统设定的默认最大值)。
- 任务数量超出设置的最大并发操作数量时,其它任务等待执行,且有其它任务完成后再加入执行。
- maxConcurrentOperationCount 为负数,且不为-1时,会崩掉。
代码:
// MARK: - 队列任务串行
func serialQueue() {
let queue: OperationQueue = OperationQueue()
// 设置为1时,串行
// 大于1时,并发
// 默认为 -1,且不可设置为其它负数
queue.maxConcurrentOperationCount = 1
// 另一种添加任务的写法
let op1 = BlockOperation {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
let op2 = BlockOperation {
for i in 0...2 {
print("\(i) -> +++++++++++++++ ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
let op3 = BlockOperation {
for i in 0...2 {
print("\(i) -> >>>>>>>>>>>>>>> ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation(op3)
}
执行结果:
我们在贴上一个queue.maxConcurrentOperationCount = 2 时的打印结果:
该情况是queue.maxConcurrentOperationCount 小于 任务操作数,此时并发执行两个操作,当有空余资源时,再执行第三个。
5、线程依赖
Operation 的封装使用起来很简单,所以我们直接上代码:
// MARK: - 添加任务依赖
func addDependency() {
let queue: OperationQueue = OperationQueue()
// 创建操作任务
let op1 = BlockOperation {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
let op2 = BlockOperation {
for i in 0...2 {
print("\(i) -> +++++++++++++++ ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
let op3 = BlockOperation {
for i in 0...2 {
print("\(i) -> >>>>>>>>>>>>>>> ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
}
// 添加任务依赖
op1.addDependency(op2)
op2.addDependency(op3)
// 任务添加到队列执行
queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation(op3)
}
贴一张打印结果:
6、操作任务优先级
- 优先级取值
// 由上到下优先级依次变高
// swift
public enum QueuePriority : Int {
case veryLow
case low
case normal
case high
case veryHigh
}
// OC
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
长文本内容需耐心阅读加深理解,下方概念性文本参考该文章
Operation 的 queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。
上边我们说过:对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
那么,什么样的操作才是进入就绪状态的操作呢?
当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。
划重点:
- queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
- 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
- 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。
- 优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。
7、异步操作后返回主线程(线程通信)
在GCD中我们经常在异步执行完某个任务后,返回主线程刷新UI操作,如:
// 异步操作后返回主线程
func communication_GCD() {
DispatchQueue.global().async {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
DispatchQueue.main.async {
print("回到主线程")
}
}
}
在Operation中同样也会经常用到:
// 异步操作后返回主线程
func communication() {
let queue: OperationQueue = OperationQueue()
queue.addOperation {
for i in 0...2 {
print("\(i) -> *************** ", Thread.current)
Thread.sleep(forTimeInterval: 1)
}
OperationQueue.main.addOperation({
print("回到主线程")
})
}
}
附上结果打印图一张:
8、线程锁
提到线程锁,经常会拿售票举例。那好,我们回顾理解一下。
(1)没有线程锁操作
/// 剩余票数
var surplusTicketCount: Int = 100
// 写三个线程模拟卖票的窗口
func saleTicket() {
// 第一个售票窗口
let queue_1 = OperationQueue()
queue_1.maxConcurrentOperationCount = 1
queue_1.addOperation {
self.saleAction()
}
// 第二个售票窗口
let queue_2 = OperationQueue()
queue_2.maxConcurrentOperationCount = 1
queue_2.addOperation {
self.saleAction()
}
// 第三个售票窗口
let queue_3 = OperationQueue()
queue_3.maxConcurrentOperationCount = 1
queue_3.addOperation {
self.saleAction()
}
}
// 计票操作
func saleAction() {
while true {
if surplusTicketCount > 0 {
//如果还有票,继续售卖
surplusTicketCount -= 1
print("剩余票数 ---> \(surplusTicketCount) 线程:\(Thread.current)")
// 模拟卖一张票的耗时
Thread.sleep(forTimeInterval: 0.1)
} else {
print("火车票卖完了!!!")
break;
}
}
}
售卖结果:
很明显有重复售票的情况
(2)加入线程锁售票
/// 剩余票数
var surplusTicketCount: Int = 100
func saleTicket() {
// 第一个售票窗口
let queue_1 = OperationQueue()
queue_1.maxConcurrentOperationCount = 5
queue_1.addOperation {
self.saleAction()
}
// 第二个售票窗口
let queue_2 = OperationQueue()
queue_2.maxConcurrentOperationCount = 5
queue_2.addOperation {
self.saleAction()
}
// 第三个售票窗口
let queue_3 = OperationQueue()
queue_3.maxConcurrentOperationCount = 5
queue_3.addOperation {
self.saleAction()
}
}
// 线程锁
let lock = NSLock()
func saleAction() {
while true {
lock.lock()
if surplusTicketCount > 0 {
//如果还有票,继续售卖
surplusTicketCount -= 1
print("剩余票数 ---> \(surplusTicketCount) 线程:\(Thread.current)")
Thread.sleep(forTimeInterval: 0.1)
}
lock.unlock()
if surplusTicketCount <= 0 {
print("火车票卖完了!!!")
break;
}
}
}
售票结果:
代码中我们将每个队列的最大并发数量设置为了5,即使这样也没有了重复售票的情况。
9、任务取消、继续和状态
(1) NSOperation 常用属性和方法
取消操作方法
- (void)cancel; 可取消操作,实质是标记 isCancelled 状态。
判断操作状态方法
- (BOOL)isFinished; 判断操作是否已经结束。
- (BOOL)isCancelled; 判断操作是否已经标记为取消。
- (BOOL)isExecuting; 判断操作是否正在在运行。
- (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。操作同步
- (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。
- (void)setCompletionBlock:(void (^)(void))block; completionBlock 会在当前操作执行完毕时执行 completionBlock。
- (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
- (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
@property (readonly, copy) NSArray
(2) NSOperationQueue 常用属性和方法
取消/暂停/恢复操作
- (void)cancelAllOperations; 可以取消队列的所有操作。
- (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。
- (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。操作同步
- (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。
添加/获取操作`
- (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象。
- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
- (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。
- (NSUInteger)operationCount; 当前队列中的操作数。
获取队列
+ (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。
+ (id)mainQueue; 获取主队列。
注意:这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。