- pthread
- NSThread
- GCD
1. 同步、异步、并发、串行讲解
2. 创建队列的几种方式
3. 栅栏函数
4. 队列组
5. GCD快速迭代 - NSOperation和NSOperationQueue
1. NSInvocationOperation和NSBlockOperation
2. NSOperationQueue
3. 任务依赖 - GCD和NSOperation的比较
- 多线程的安全隐患
关于多线程,在 iOS 中目前有 4 套方案,他们分别是:
下面我们分别来为大家一一介绍上述方案:
方案一:pthread
#import <pthread.h>
//创建线程对象
pthread_t thread = NULL;
//传递的参数
id str = @"i'm pthread param";
//创建线程
/* 参数一:线程对象 传递线程对象的地址
参数二:线程属性 包括线程的优先级等
参数三:子线程需要执行的方法
参数四:需要传递的参数
*/
int result = pthread_create(&thread, NULL, operate, (__bridge void *)(str));
if (result == 0) {
NSLog(@"创建线程 OK");
} else {
NSLog(@"创建线程失败 %d", result);
}
//手动把当前线程结束掉
// pthread_detach:设置子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
pthread_detach(thread);
void *operate(void *params){
NSString *str = (__bridge NSString *)(params);
NSLog(@"%@ - %@", [NSThread currentThread], str);
return NULL;
}
方案二:NSThread
- 先创建线程类,再启动
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
// 启动
[thread start];
- 创建后立即启动
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
方案三:GCD
它是苹果为多核的并行运算提出的解决方案,所以它会自动合理地利用更多的CPU内核,最重要的是它会自动管理线程的生命周期(比如创建线程、调度任务、销毁线程)
1. 同步、异步、并发、串行讲解
GCD中有2个用来执行任务的函数
用同步的方式执行任务
//queue:队列 block:任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
容易混淆的术语
有4个术语比较容易混淆:同步
、异步
、并发
、串行
同步和异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力(但是不一定能够开启新线程,比如异步在主队列中执行任务)
并发和串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务
各种队列的执行效果
注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
2. 创建队列的几种方式
-
主队列: 它是一个特殊的
串行队列
, 任何需要刷新 UI 的工作都要在主队列执行。
dispatch_queue_t queue = dispatch_get_main_queue();
-
自定义队列: 自己可以创建
串行队列
, 也可以创建并行队列
。
//串行队列
dispatch_queue_t queue = dispatch_queue_create("test1", NULL);
dispatch_queue_t queue = dispatch_queue_create("test2", DISPATCH_QUEUE_SERIAL);
//并行队列
dispatch_queue_t queue = dispatch_queue_create("test3", DISPATCH_QUEUE_CONCURRENT);
- 全局并行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3. 验证一下所学知识
下面我们来看看下面几段代码,猜一猜运行之后结果是个啥子嘛。。。
考题一:
NSLog(@"任务一");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任务二");
});
NSLog(@"任务三");
额。。。知道结果了么?下面我们来揭晓答案
执行结果:
为什么会这样呢?
因为同步任务会阻塞当前线程,然后把 Block 中的任务放到主队列中执行,队列是FIFO
,所以Block中的任务只有等到dispatch_sync
执行完毕后才会执行,但是dispatch_sync
要想执行完成必须Block中的任务执行完毕后才会结束.这就是非常经典的死锁现象.
考题二:
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"任务一");
dispatch_async(queue, ^{
NSLog(@"任务二");
dispatch_sync(queue, ^{
NSLog(@"任务三");
});
NSLog(@"任务四");
});
NSLog(@"任务五");
看了考题一的分析 我相信考题二难不住你的,我们来看看打印结果:
其实原因跟上一个例子我们分析的原因类似,记住这句结论就好:
注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
3. 栅栏函数
在项目中有很多场景需要控制任务的执行顺序,比如需要等任务A, 任务B, 任务C都完成后(其中A, B, C没有顺序要求), 才进行下一步的处理任务, 可以使用 dispatch_group很方便的完成 (也可以使用栅栏函数)
如果上面的A, B, C任务顺序也有顺序要求呢? 必须A任务完成后, 才能进行B任务, B完成后才进行C任务, 这时我们就需要用到栅栏函数
dispatch_barrier_async
:在进程管理中起到一个栅栏
的作用,该函数需要同dispatch_queue_create
函数生成的并发队列
一起使用才能生效。
第一种情况
A, B, C任务完成之后(A, B, C无顺序要求), 进行任务D
1.使用dispatch_barrier
dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"开始任务A");
[NSThread sleepForTimeInterval:1];
NSLog(@"任务A done.");
});
dispatch_async(queue, ^{
NSLog(@"开始任务B");
[NSThread sleepForTimeInterval:0.5];
NSLog(@"任务B done.");
});
dispatch_async(queue, ^{
NSLog(@"开始任务C");
[NSThread sleepForTimeInterval:0.2];
NSLog(@"任务C done.");
});
dispatch_barrier_async(queue, ^{
NSLog(@"----------> barrier <----------");
});
dispatch_async(queue, ^{
NSLog(@"开始任务D");
});
打印结果:
开始任务C
开始任务A
开始任务B
任务C done.
任务B done.
任务A done.
----------> barrier <----------
开始任务D
可以看出在执行完栅栏前面的操作之后才执行栅栏操作,然后再执行栅栏后边的操作。
如果不加barrier 函数, 输出如下:
开始任务B
开始任务A
开始任务D
开始任务C
任务C done.
任务B done.
任务A done.
dispatch_barrier_async和dispatch_barrier_sync使用区别:
dispatch_barrier_async和dispatch_barrier_sync是 GCD 中的两个方法。是不是和dispatch_async及dispatch_sync长得很像,就是多了一个barrier(译:栅栏)。
没错,除了有dispatch_async或dispatch_sync的作用外(是否阻塞当前线程),还有“栅栏”的效果。
意思就是,在该队列,以他们为界,待前面任务执行完成,再把自己内部的任务执行完,才会执行后面的任务。
知道和dispatch_async及dispatch_sync对应,就应该想到:
dispatch_barrier_async不阻塞当前线程,dispatch_barrier_async里面的任务异步执行。
dispatch_barrier_sync会阻塞当前线程,dispatch_barrier_sync里面的任务同步执行。
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQ", DISPATCH_QUEUE_CONCURRENT);
//以下任务
dispatch_async(concurrentQueue, ^{ NSLog(@"任务1"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"任务2"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"任务3"); });
dispatch_barrier_async(concurrentQueue, ^{
sleep(1);
NSLog(@"I am barrier");
});
NSLog(@"当前线程");
dispatch_async(concurrentQueue, ^{ NSLog(@"任务4"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"任务5"); });
输出结果:
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQ", DISPATCH_QUEUE_CONCURRENT);
//以下任务
dispatch_async(concurrentQueue, ^{ NSLog(@"任务1"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"任务2"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"任务3"); });
dispatch_barrier_sync(concurrentQueue, ^{
sleep(1);
NSLog(@"I am barrier");
});
NSLog(@"当前线程");
dispatch_async(concurrentQueue, ^{ NSLog(@"任务4"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"任务5"); });
输出结果
2.使用 dispatch_group
dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"开始任务A");
[NSThread sleepForTimeInterval:3];
NSLog(@"任务A done.");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"开始任务B");
[NSThread sleepForTimeInterval:2];
NSLog(@"任务B done.");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"开始任务C");
[NSThread sleepForTimeInterval:1];
NSLog(@"任务C done.");
dispatch_group_leave(group);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"开始任务D");
});
输出如下:
开始任务C
开始任务B
开始任务A
任务C done.
任务B done.
任务A done.
开始任务D
第二种情况,任务依赖
A, B, C任务完成之后(A, B, C顺序要求依次执行), 进行任务D
dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(queue, ^{
NSLog(@"开始任务A");
[NSThread sleepForTimeInterval:1];
NSLog(@"任务A done.");
});
dispatch_barrier_async(queue, ^{
NSLog(@"开始任务B");
[NSThread sleepForTimeInterval:0.5];
NSLog(@"任务B done.");
});
dispatch_barrier_async(queue, ^{
NSLog(@"开始任务C");
[NSThread sleepForTimeInterval:0.2];
NSLog(@"任务C done.");
});
dispatch_barrier_async(queue, ^{
NSLog(@"开始任务D");
});
输出如下:
开始任务A
任务A done.
开始任务B
任务B done.
开始任务C
任务C done.
开始任务D
在每个网络请求开始前使用
dispatch_group_enter
来进行标识,网络请求有回调后使用dispatch_group_leave
来进行标识,这样就能保证group_notify
在所有网络请求都有回调之后才调用
5. GCD快速迭代
我们知道for循环
中的代码是串行执行的,如果此时我们有一系列的耗时操作需要执行,此时我们可以使用Dispatch_apply
函数,他可以异步执行,同时可以利用多核优势,完美替代for循环。
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"%zd = %@",index,[NSThread currentThread]);
});
执行结果如下:
可以看到上述循环是在多个线程中并发执行的。
6. 考题:猜测打印结果
考题一:
- (void)test{
NSLog(@"任务B");
}
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务A");
[self performSelector:@selector(test) withObject:nil afterDelay:1.0];
NSLog(@"任务C");
});
}
知道结果了么?
我们来看看打印结果:
为什么只输出了任务A和任务C而没有任务B呢?其实这里涉及到了
RunLoop的知识
,因为performSelector:withObject:afterDelay:
的本质是向RunLoop
中添加定时器,而子线程中默认是没有开启RunLoop
的,所以这里我们需要稍微改动下代码,如下;
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务A");
[self performSelector:@selector(hahha) withObject:nil afterDelay:1.0];
NSLog(@"任务C");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
}
关于RunLoop
有兴趣的朋友可以看看我的这篇文章: RunLoop的使用
考题二:
- (void)test{
NSLog(@"任务B");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"任务A");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
执行结果:
[73860:11959832] 任务A
[73860:11959410] *** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'
因为我们在执行完[thread start];
的时候执行任务A,此时线程就被销毁了,如果我们要在thread
线程中执行test
方法需要保住该线程的命,即线程保活
,代码需要修改如下:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"任务A");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
方案四:NSOperation和NSOperationQueue
NSOperation
是苹果公司对 GCD面向对象的封装,所以使用起来非常方便。
NSOperation
和NSOperationQueue
分别对应 GCD 的 任务 和 队列 。
使用步骤大致如下:
- 先将需要执行的操作封装到一个NSOperation对象中
- 然后将NSOperation对象添加到NSOperationQueue中
- 系统会⾃动将NSOperationQueue中的NSOperation取出来
- 将取出的NSOperation封装的操作放到⼀条新线程中执⾏
1. 任务
NSOperation
只是一个抽象类,所以不能封装任务。
但是我们可以使用它的两个子类对象:NSInvocationOperation
、NSBlockOperation
。
- NSInvocationOperation : 需要传入一个方法名。
//1.创建NSInvocationOperation对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
//2.开始执行
[operation start];
打印结果:
其实等价于[self run];
在主线程中执行。
如果我们想让任务在子线程中执行,我们需要创建一个NSOperationQueue
,如下:
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
// 添加操作到队列中,会自动异步执行
[queue addOperation:operation];
打印结果:
注意:操作对象默认在主线程中执行,只有将NSOperation
放到一个 NSOperationQueue
中,才会异步执行操作
- NSBlockOperation:用来并发的执行一个或者多个Block对象。
注意:addExecutionBlock:
该方法只要NSBlockOperation封装的操作数 > 1,就会异步执行操作
//1.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//2.开始任务
[operation start];
打印结果:
<NSThread: 0x604000074780>{number = 1, name = main}
addExecutionBlock方式添加多个任务:
NSBlockOperation *operation = [[NSBlockOperation alloc] init];
[operation addExecutionBlock:^{
//---下载图片----1---<NSThread: 0x600000260fc0>{number = 1, name = main}
NSLog(@"---下载图片----1---%@", [NSThread currentThread]);
}];//这种方式只有第一个是主线程,其余都是子线程
[operation addExecutionBlock:^{
//---下载图片----2---<NSThread: 0x600000263f40>{number = 3, name = (null)}
NSLog(@"---下载图片----2---%@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^{
//---下载图片----3---<NSThread: 0x60800026c440>{number = 4, name = (null)}
NSLog(@"---下载图片----3---%@", [NSThread currentThread]);
}];
[operation start];
2. 队列
通过上面的介绍我们知道调用NSOperation
对象的start()
方法可以启动任务,但是这样做他们默认是 同步执行 的。即使是addExecutionBlock
方法,也会在 当前线程和其他线程 中执行,也就是说还是会占用当前线程。此时我们就需要用到NSOperationQueue
了。
只要任务添加到队列,便会自动调用任务的start()
方法
//1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"---下载图片----1---%@", [NSThread currentThread]);
}];
[operation addExecutionBlock:^{
NSLog(@"---下载图片----2---%@", [NSThread currentThread]);
}];
// 2.添加操作到队列中(自动异步执行)
[queue addOperation:operation];
打印结果:
任务依赖
需求:此时有 3 个任务,这三个任务因为比较耗时,所以需要异步并发执行。
任务一: 从服务器上下载一张图片
任务二:给这张图片加个水印
任务三:把图片返回给服务器。
这时候就需要控制任务的执行顺序了
//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
打印结果
- 注意:不能添加相互依赖,比如 A依赖B,B又依赖A,否则会造成死锁
1. 从其他线程回到主线程的方法
我们都知道在其他线程操作完成后必须到主线程更新UI。所以,介绍完所有的多线程方案后,我们来看看有哪些方法可以回到主线程。
- NSThread
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
- GCD
dispatch_async(dispatch_get_main_queue(), ^{
});
- NSOperationQueue
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
}];
2. 延迟执行方案
公用延迟执行方法
- (void)delayMethod{
NSLog(@"delayMethodEnd");
}
线程阻塞式
1.NSThread线程的sleep
[NSThread sleepForTimeInterval:2.0];
此方法是一种阻塞执行方式,建议放在子线程中执行,否则会卡住界面。但有时还是需要阻塞执行,比如进入欢迎界面需要沉睡2秒才进入主界面时。
非阻塞执行方式
performSelector
[self performSelector:@selector(delayMethod) withObject:nil afterDelay:2.0];
NSTimer定时器
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];
-
GCD
的方式
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
[weakSelf delayMethod];
});
此方法可以在参数中选择执行的线程,是一种非阻塞执行方式
GCD和NSOperation的比较
GCD 和 NSOperation的区别主要表现在以下几方面:
1、GCD是一套 C 语言API,执行和操作简单高效,NSOperation底层也通过GCD实现,这是他们之间最本质的区别,因此如果希望自定义任务,建议使用NSOperation;
2、依赖关系,NSOpeartion可以通过addDependency来添加任务的依赖,GCD需要添加依赖只能通过dispatch_barrier_async
3、KVO(键值对观察),可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)对此GCD无法通过KVO进行判断;
4、优先级,NSOpeartion可以设置queuePriority来设置优先级,跳转任务的执行先后顺序,GCD只能设置队列的优先级,且任务是根据先进先出FIFO的原则来执行的,不能设置任务的优先级。
5、继承,NSOperation是一个抽象类。实际开发中常用的是它的两个子类:NSInvocationOperation和NSBlockOperation,同样我们可以自定义NSOperation,GCD执行任务可以自由组装,没有继承那么高的代码复用度;
6、效率,直接使用GCD效率确实会更高效,NSOperation会多一点开销,但是通过NSOperation可以获得依赖,优先级,继承,键值对观察这些优势,相对于多的那么一点开销确实很划算,鱼和熊掌不可得兼,取舍在于开发者自己;
7、NSOperation可以设置暂停,挂起等操作,可以随时取消准备执行的任务(已经在执行的不能取消),GCD没法停止已经加入queue 的 block(虽然也能实现,但是需要很复杂的代码)
基于GCD简单高效,更强的执行能力,操作不太复杂的时候,优先选用GCD;而比较复杂的任务可以自己通过NSOperation实现。
8、NSOperation可以设置最大任务数,
多线程的安全隐患
当多个线程同时访问同一个资源时,很容易引发数据错乱和数据安全问题,比如下图:
那么我们该如何去解决这个问题呢?
我们可以使用线程同步技术
。所谓同步,就是协同步调,按预定的先后次序进行。常见的线程同步技术就是加锁
关于锁的实现方案 网上有很多,这里我就不再列举了,可以参考:
iOS中保证线程安全的几种方式与性能对比
iOS 常见知识点(三):Lock
深入理解iOS开发中的锁