写一下iOS多线程的相关知识,多线程无论是在实际开发中还是在面试的时候都是相关重要的一个知识点,特别是GCD和NSOperation,现在做一下总结,加深一下对多线程的理解。
概述
在此,我就不说什么是多线程、线程和进程的区别、多线程有什么作用、还有串行和并行的相关内容,主要说NSThreads、GCD、NSOperation和NSOperationQueue的使用方法和一些案例,其实除了上面三套线程方案外,还有一个Pthreads,在类Unix操作系统,都使用Pthreads作为操作系统的线程,我是没用过,就不再这班门弄斧了,大家自行查资料。
NSThread
这方案是苹果封装后的,并完全面向对象的,可以直接操控线程对象,非常直观和方便,但它的生命周期需要我们手动管理,这个方案也是偶尔调试用用([NSThread currentThread]),获取当前线程类,可以看到当前线程的各种属性。
创建并启动
- 先创建线程类->启动
OC
NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(eat:) object:nil];
[thread start];
//创建并自动启动
[NSThread detachNewThreadSelector:@selector(eat:) toTarget:self withObject:nil];
Swift
let thread = Thread(target: self, selector: #selector(eat), object: nil)
thread.start()
// 创建并自动启动
Thread.detachNewThreadSelector(#selector(eat), toTarget: self, with: nil)
还有一些常见的方法
// 取消线程
[thread cancel];
// 获取当前线程信息
[NSThread currentThread];
// 获取主线程信息
[NSThread mainThread];
其实,NSThread用起来很简单,方法比较少,也只会在一些简单的场景才会用这个,不能优雅地处理多线程中的其他高级概念。接下来要说的就是比较重点的了。
GCD
Grand Central Dispatch,是苹果为多核的并行运算提出的解决方案,会自动合理地利用更多CPU内核,最重要的是会自动管理线程的生命周期(创建线程、调度任务和销毁线程),不需要我们管理,只需要告诉干什么就行,GCD使用的是C语言,由于使用了Block,使用起来很方便,这个也是我用的最多的一种方案。
任务和队列
在GCD中,加入了两个很重要的概念:任务和队列。
- 任务:就是要干什么,在此就是一个Block,任务有两种执行方式:同步执行和异步执行,区别是是否会阻塞当前线程,知道任务执行完毕。
同步执行:会阻塞当前线程并等待Block中的任务执行完毕,然后当前线程才会继续往下运行。
异步执行:会直接往下执行,不会阻塞当前线程。
- 队列:用于存放任务。有串行队列和并行队列
串行队列的任务会根据队列的定义FIFO执行,也就是取一个出来,执行一个,然后再取一个出来,一个一个执行。
并行队列也会FIFO执行,不同的是,它取出来一个会放到别的线程,然后再取一个又放到另一个线程,取的动作很快,看起来都是一起执行的。
创建队列
- 主队列:这是一个特殊的串行队列。用于刷新UI,任何需要刷新UI的工作都要在主队列执行,所以一般耗时的任务都要放在别的线程执行。
// OC
dispatch_queue_t queue = dispatch_get_main_queue();
// Swift
let queue = dispatchMain()
- 自己创建的队列: 第一个参数是标识符,用于DEBUG的时候标识唯一的队列,可以为空,第二个参数传入 DISPATCH_QUEUE_SERIAL 或 NULL 标识创建串行队列。传入 DISPATCH_QUEUE_CONCURRENT 标识创建并行队列。
OC
// 串行队列
dispatch_queue_t queue1 = dispatch_queue_create("FIRSTQUEUE", NULL);
dispatch_queue_t queue2 = dispatch_queue_create("SECONDQUEUE", DISPATCH_QUEUE_SERIAL);
// 并行队列
dispatch_queue_t queue3 = dispatch_queue_create("THIRDQUEUE", DISPATCH_QUEUE_CONCURRENT);
Swift
// 串行队列
let queue1 = DispatchQueue(label: "serialQueuel")
// 并行队列
let queue2 = DispatchQueue(label: "concurrentQueuel", qos: .default, attributes: .concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.never, target: nil)
- 全局并行队列: 只要是并行任务一般都加入到这个队列。这是系统提供的一个并发队列。
OC
dispatch_queue_t queue4 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
Swift
let queue3 = DispatchQueue.global(qos: .default)
创建任务
- 同步任务:会阻塞当前线程(sync)
Objective-C
dispatch_queue_t queue3 = dispatch_queue_create("THIRDQUEUE", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue3, ^{
NSLog(@"%@",[NSThread currentThread]);
});
Swift
let queue2 = DispatchQueue(label: "concurrentQueuel", qos: .default, attributes: .concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.never, target: nil)
queue2.sync {
print(Thread.current)
}
- 异步任务:不会阻塞当前线程(async)
Objective-C
dispatch_queue_t queue3 = dispatch_queue_create("THIRDQUEUE", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue3, ^{
NSLog(@"%@",[NSThread currentThread]);
});
Swift
let queue2 = DispatchQueue(label: "concurrentQueuel", qos: .default, attributes: .concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.never, target: nil)
queue2.async {
print(Thread.current)
}
为了更好理解同步和异步,举两个栗子:
eg1:
NSLog(@"当前线程 --- %@",[NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"同步 --- %@",NSThread.currentThread);
});
NSLog(@"现在线程 --- %@",NSThread.currentThread);
MultithreadingOC[1071:46482] 当前线程 --- <NSThread: 0x604000063680>{number = 1, name = main}
很尴尬,打印完第一句之后,卡死在block那里了,为什么会这样呢?
Answer
同步任务会阻塞当前线程,然后把Block中的任务放到指定的队列中执行,只能等Block中的任务完成才能继续往下运行,在打印了第一句之后,dispatch_sync 立即阻塞了主线程,Block中的任务放到main中,可是main中的任务会被取出来放到主线程中执行,但主线程这个时候已经被阻塞了,所以Block中的任务就不能完成,然后就会一直阻塞主线程,出现死锁现象。
eg2:
print("之前 - \(Thread.current)")
let queue = DispatchQueue(label: "SERIAL")
queue.async {
print("sync之前 - \(Thread.current)")
queue.sync(execute: {
print("sync - \(Thread.current)")
})
print("sync之后 - \(Thread.current)")
}
print("之后 - \(Thread.current)")
输出如下:
之前 - <NSThread: 0x604000077940>{number = 1, name = main}
sync之前 - <NSThread: 0x604000274100>{number = 3, name = (null)}
之后 - <NSThread: 0x604000077
其中有两条打印信息没有被打印,接下来,我们分析一下:
- 首先我们创建了一个串行队列。
- ‘dispatch_async’ 是异步操作,所以不像第一个例子那样阻塞当前线程,开辟了多一条线程,一条当前线程继续往下打印了‘之后 - ’那句,另一条执行Block中的内容,打印了‘sync之前 - ’那句,应为是并行,所以打印顺序先后是无所谓的。
- 但是,中间那里,‘dispatch_sync’ 同步执行了,‘sync’ 把Block 的任务放到 ‘queue’ 中,可队列是串行队列,一次执行一个任务,可 ‘queue’ 被 ‘sync’ 阻塞了,又发生死锁了,所以不会打印里面两句代码。
队列组
队列组可以将很多队列添加到一个组里,当这个组里所有任务执行完了,队列组会通过一个方法通知我们。
Objective-C
// 1. 创建队列组
dispatch_group_t group = dispatch_group_create();
// 2. 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 3. 多次使用队列组的方法执行任务,只有异步方法
// 3.1 执行3次循环
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"group - %@",[NSThread currentThread]);
}
});
// 3.3 执行5次循环
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"group - %@",[NSThread currentThread]);
}
});
// 4. 都完成了自动通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"end - %@",NSThread.currentThread);
});
Swift
let group = DispatchGroup()
let queue = DispatchQueue.global()
queue.async(group: group) {
for _ in 0..<3 {
print("group1 - \(Thread.current)")
}
}
queue.async(group: group) {
for _ in 0..<5 {
print("group2 - \(Thread.current)")
}
}
group.notify(queue: DispatchQueue.main) {
print("end - \(Thread.current)")
}
以上就是GCD的基本功能,但它的能力远不止这些,讲完NSOperation后,在看其他一些用途。
NSOperation&NSOperationQueue
NSOperation是苹果对GCD的封装,完全面向对象,用起来更好理解。
添加任务
NSOperation是一个抽象类,不能封装任务,但它有两个子类用于封装任务。分别是:NSInvocationOperation和NSBlockOperation。创建Operation后,需要调用start方法来启动任务,会默认在当前队列同步执行。也可以中途取消任务,调用cancel即可。
- NSInvocationOperation:需要传入一个方法名。
NSInvocationOperation * operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
[operation start];
在Swift中,NSInvocationOperation不是ARC-safe,所以不支持。
- NSBlockOperation
OC
// 1. 创建NSBlockOperation
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
// 2. 开始任务
[operation start];
Swift
// 1. 创建BlockOperation
let operation = BlockOperation {
print(Thread.current)
}
// 2. 开始任务
operation.start()
上面说了,这样创建任务,会默认在当前线程执行。但是NSBlockOperation还有一个方法addExecutionBlock: ,同个这个方法可以给Operation添加多个执行Block,这样Operation中的任务会并发执行。
OC
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",NSThread.currentThread);
}];
[operation addExecutionBlock:^{
NSLog(@"Other - %@",NSThread.currentThread);
}];
[operation start];
Swift
// 1. 创建BlockOperation
let operation = BlockOperation {
print(Thread.current)
}
operation.addExecutionBlock {
print("Other - \(Thread.current)")
}
// 2. 开始任务
operation.start()
注意:addExecutionBlock必须在start之前执行,否则会报错。
- 自定义Operation
除了上面两种Operation外,我们还可以自定义Operation。自定义Operation需要继承NSOperation类,并实现main()方法,因为在调用start()方法的时候,内部会调用main()方法完成相关逻辑。除此之外,还需要实现cancel()在内的各种方法。
创建队列
上面用NSOperation启动任务默认是同步的,就算用addExecutionBlock方法,也会在当前线程和其他线程中执行,还是会占用当前线程,这就要用到NSOperationQueue了。
- 主队列
// OC
NSOperationQueue * queue = [NSOperationQueue mainQueue];
// swift
let queue = OperationQueue.main
- 其他队列
OC
// 1. 创建一个队列
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
// 2. 创建NSBlockOperation对象
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@",NSThread.currentThread);
}];
[operation addExecutionBlock:^{
NSLog(@"%@",NSThread.currentThread);
}];
// 3. 队列添加任务
[queue addOperation:operation];
Swift
let queue = OperationQueue()
let operation = BlockOperation {
print("--- \(Thread.current)")
}
operation.addExecutionBlock {
print("\(Thread.current) ---")
}
queue.addOperation(operation)
对比GCD会发现,串行队列找不到了,那怎么办呢?
NSOperationQueue有一个参数maxConcurrentOperationCount最大并发数,可以设置多少个任务同时执行,我们把它设为1,那想不串行都不行了。
NSOperation有一个很实用的功能,就是添加依赖。例如:1:从服务器download一张图片,2:对图片进行加水印,3:把图片upload到服务器
OC
NSBlockOperation * operationOne = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download - %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
NSBlockOperation * operationTwo = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"add - %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
NSBlockOperation * operationThree = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"upload - %@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
// 设置依赖
[operationTwo addDependency:operationOne];
[operationThree addDependency:operationTwo];
// 创建队列加入任务
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operationThree, operationTwo, operationOne] waitUntilFinished:NO];
Swift
let operationOne = BlockOperation {
print("download - \(Thread.current)")
Thread.sleep(forTimeInterval: 1.0)
}
let operationTwo = BlockOperation {
print("add - \(Thread.current)")
Thread.sleep(forTimeInterval: 1.0)
}
let operationThree = BlockOperation {
print("upload - \(Thread.current)")
Thread.sleep(forTimeInterval: 1.0)
}
operationTwo.addDependency(operationOne)
operationThree.addDependency(operationTwo)
let queue = OperationQueue()
queue.addOperations([operationThree, operationTwo, operationOne], waitUntilFinished: false)
输出
2018-04-12 17:14:29.523538+0800 MultithreadingOC[4136:252621] download - <NSThread: 0x600000263ec0>{number = 3, name = (null)}
2018-04-12 17:14:30.526059+0800 MultithreadingOC[4136:252621] add - <NSThread: 0x600000263ec0>{number = 3, name = (null)}
2018-04-12 17:14:31.527632+0800 MultithreadingOC[4136:252619] upload - <NSThread: 0x604000279580>{number = 4, name = (null)}
要注意的是不能互相依赖,会死锁,可以通过removeDependency来接触依赖关系。
以上就是NSOperation的一些基本用法,接下来说一下关于多线程的一些应用,尽量用多种方法实现以下。
单例模式
什么是单例,我就不多说了,直入主题
Objective-C
@implementation Single
static id _instance;
+ (instancetype)sharedSingle{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[Single alloc] init];
});
return _instance;
}
@end
Swift
import UIKit
class Single: NSObject {
static let sharedSingle = Single()
private override init() {
}
}
延迟执行
延时一段时间再执行某段代码。
// perform
[self performSelector:@selector(run:) withObject:@"123" afterDelay:3];
// GCD
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{
});
// NSTimer
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
线程同步
线程同步是为了防止多个线程抢夺同一个资源造成的数据安全问题。
- 互斥锁:给需要同步的代码块加一个互斥锁,可以保证每次只有一个线程访问此代码块。
OC
- (void)run{
@synchronized(self) {
// 需要执行的代码块
}
}
Swift
@objc func eat() {
objc_sync_enter(self)
// 需要执行的代码块
objc_sync_exit(self)
}
- 同步执行:将多个线程都要执行此段代码添加到同一个串行队列,就可以实现线程同步了。
GCD
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
});
NSOperation
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
}];
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
queue.maxConcurrentOperationCount = 1;
[operation waitUntilFinished];
总结一下
写了两天,写的都是一些比较基础的东西,Swift版本和OC版本差别不是特别大,,但是关于多线程的东西还远不止于此,以后用到其他的方法会继续更新上去的~~~