目录
1.NSThread
2.NSOperation
3.GCD
https://www.cnblogs.com/kenshincui/p/3983982.html
https://juejin.cn/post/7065961731461382151
1.NSThread
NSThread 是轻量级的多线程开发;NSThread 是一个对 pthread 对象化的封装,是苹果官方提供面向对象操作线程的技术,简单易用。使用 OC 语言,可以直接操作线程对象,不过需要自己管理线程生命周期。
NSThread 详解:https://juejin.cn/post/6844903556009443335
相关代码
1.实现方法
1.1.用类方法直接将操作添加到线程中并启动:+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument
1.2.用对象方法创建一个线程对象,然后调用 start 方法启动线程:- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
//方法1:
[NSThread detachNewThreadSelector:@selector(foo:) toTarget:self withObject:sender];
//方法2:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(foo:) object:sender];
[thread start];//启动线程(执行foo回调方法)
//sleep(1);
//提示: 如果线程已经开始执行则无法取消
//[thread cancel];
具体看课程代码
2.考虑开辟多条线程竞争同一资源(或多个资源,这考虑同1的情况)
//可在AppDelegate.m中写
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 创建一个可变字符串作为多个线程共同竞争的一个资源
NSMutableString *mStr = [NSMutableString stringWithCapacity:50000];
// 创建5个线程操作同一个资源
for (int i = 1; i <= 5; i++) {
[NSThread detachNewThreadSelector:@selector(foo:) toTarget:self withObject:mStr];
}
return YES;
}
- (void) foo:(NSMutableString *) mStr {
for (int i = 0 ; i < 10000; i++) {
// 提示: 如果多个线程竞争同一个资源通常需要进行同步保护
// 同步: 只让一个线程获得资源使用完毕后下一个线程才能获得(排队方式)
// 该资源其他线程仍然等待使用资源的线程释放资源
@synchronized (mStr) {
[mStr appendString:@"a"];
}
}
}
3.常见写法
[NSThread isMultiThreaded];//是否开启了多线程
[NSThread mainThread];//获取主线程
[NSThread currentThread];//获取当前线程
[NSThread sleepForTimeInterval:3];//线程睡眠3s
2.NSOperation
NSOperation 是基于 GCD 更高一层的封装,使用更加面向对象。使用 OC 语言,自动管理生命周期。比 GCD 可控性更强,可以加入操作依赖控制执行顺序、设置操作队列最大并发数、取消操作等。
其他说明:
1.使用 NSOperation 和 NSOperationQueue 进行多线程开发类似于 C# 中的线程池,只要将一个 NSOperation(实际开中需要使用其子类NSInvocationOperation、NSBlockOperation)放到 NSOperationQueue 这个队列中,线程就会依次启动。
2.NSOperationQueue 负责管理、执行所有的 NSOperation,在这个过程中可以更加容易的管理线程总数和控制线程之间的依赖关系。
3.NSOperation 有两个常用子类用于创建线程操作:NSInvocationOperation 和 NSBlockOperation。两种方式本质没有区别,但是是后者使用 Block 形式进行代码组织,使用相对方便。
NSOperation 详解:https://juejin.cn/post/6844903570467192845
- NSOperation 实现多线程分为三步:
1.创建操作:先将需要执行的操作封装到一个 NSOperation 对象中
2.创建队列:创建 NSOperationQueue 对象
3.将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中,之后系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。
- 一些概念名词
操作 Operation(相当于 GCD 的任务)
执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation,或者自定义子类来封装操作。
操作队列 Operation Queues(相当于 GCD 的队列)
1.注意这里的队列指操作队列,即用来存放操作的队列,不同于 GCD 中的调度队列 FIFO(先进先出)的原则。
解释:NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。2.操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
3.NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。
- 相关代码
使用 NSOperation 的子类 NSBlockOperation 和 NSInvocationOperation
1.举例1 - NSBlockOperation(以block形式)
//创建一个操作对象(待会要将该队列放到一个队列中去执行)
NSOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1完成");
//再创建一个队列,然后放在主队列中(就会在主线程中执行来刷新界面)
NSOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[sender setTitle:@"button1" forState:0];
}];
[[NSOperationQueue mainQueue]addOperation:op2];//主线程刷新界面
}];
//创建一个并发对列
NSOperationQueue *queue = [NSOperationQueue alloc]init];
queue.maxConcureentOperationCount = 5;//设置最大并发数量
[queue addOperation:op];//向对列中添加一个操作
2.举例2 - NSInvocationOperation
//创建一个操作对象
NSOperation *op3 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(bar:)object:button];
2.1如果使用 start,会在当前线程启动操作
[op3 start];
2.2向主队列中添加操作对象(操作放到主线程中执行)
[[NSOperationQueue mainQueue]addOperation:op3];
2.3创建一个并发对列
NSOperationQueue *queue = [NSOperationQueue alloc]init];
queue.maxConcureentOperationCount = 5;//设置最大并发数量
[queue addOperation:op3];//向对列中添加一个操作,一旦将操作添加到操作队列,操作就会启动
举例:实例化操作 NSBlockOperation
#pragma mark 模仿下载网络图像
- (IBAction)operationDemo3:(id)sender {
// 1. 下载
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载 %@" , [NSThread currentThread]);
}];
// 2. 滤镜
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"滤镜 %@" , [NSThread currentThread]);
}];
// 3. 显示
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"更新UI %@" , [NSThread currentThread]);
}];
// 使用addDependency可以设置任务的执行先后顺序,同时可以跨操作队列指定依赖关系
// 添加操作之间的依赖关系,所谓“依赖”关系,就是等待前一个任务完成后,后一个任务才能启动
// 依赖关系可以跨线程队列实现
// 提示:在指定依赖关系时,注意不要循环依赖,否则不工作。
[op2 addDependency:op1];
[op3 addDependency:op2];
[op1 addDependency:op3];
[_queue addOperation:op1];
[_queue addOperation:op2];//将操作添加到队列NSOperationQueue即可启动多线程执行
[[NSOperationQueue mainQueue] addOperation:op3];//更新UI使用主线程队列
}
3.GCD
GCD 是基于 C 语言开发的一套多线程开发机制,也是苹果推荐的多线程开发方法。三种开发中 GCD 抽象层次最高,使用也最简单,自动管理生命周期。GCD 是基于 C 语言开发,是完全面向过程,而不像 NSOperation 是面向对象开发。这种机制相比较于前面两种最显著的优点就是它对于多核运算更加有效。
其他说明:
1.是一套底层 API,GCD 的 API 很大程度上基于 block ,当配合 block 使用时,GCD 非常简单易用且能发挥其最大能力。GCD 就是为了在“多核”上使用多线程技术,GCD 所有的方法都是 dispatch 开头。
2.GCD 中也有一个类似于 NSOperationQueue 的队列,GCD 统一管理整个队列中的任务。但是 GCD 中的队列分为串行队列和并行队列两类,其实在 GCD 中还有一个特殊队列就是主队列,用来执行主线程上的操作任务(NSOperation 中也有一个主队列)。
- 串行队列:只有一个线程,加入到队列中的操作按添加顺序依次执行。
- 并行队列:有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理。
结论:
1.在 GDC 中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行。2.串行队列可以按顺序执行,并行队列的异步方法无法确定执行顺序。
3.UI界面的更新最好采用同步方法,其他操作采用异步方法。
- 1.常用相关代码
dispatch_sync//同步
dispatch_async//异步
dispatch_queue_t//主要分为两种,串行、并行
dispatch_queue_create("concurrent_queue",DISPATCH_QUEUE_CONCURRENT);//并发
dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL)//串行
dispatch_once_t//代码只会执行一次,比如用于单例
dispatch_after;//延迟操作
dispatch_get_main_queue;//回到主线程操作
//创建一个非主线线程
dispatch_queue_global_t reqQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//获取主线程
dispatch_queue_main_t mainQueue= dispatch_get_main_queue();
//异步执行操作非主线程
dispatch_async(reqQueue, ^{
//网络请求等耗时操作
//在主线程中操作
dispatch_async(mainQueue, ^{
//修改view、UI等操作
});
});
- 2.一些概念名词
global 全局、queue 队列、sync 同步、async 异步;队列有三种:全局队列、串行队列、主队列。
1.同步异步决定是否具备开启线程的能力,与方法名无关,与运行所在的队列有关,同步主要用来控制方法的被调用的顺序。
2.要执行异步的任务就在全局队列中执行即可,dispatch_async 异步执行控制不住先后顺序。
3.串行并行决定代码执行的先后顺序。
- 3.两种队列的区别:(有串行队列、并发队列;两者都符合FIFO即先进先出原则)
1.两者的执行顺序不同,以及开启的线程数不同。
2.串行是一次只有一个任务被执行;只开启一个线程,一个任务执行完毕后执行下一个任务。
3.并发是多个任务可以同时执行,可以开启多个线程同时执行任务。(注意:并发队列的并发功能只有在异步函数下才有效)
常用队列经典形式
1.全局队列 dispatch_get_global_queue(比如执行耗时的异步操作)
//可同步、可异步
dispatch_async(dispatch_get_global_queue(0,0),^{
//第一个0表示默认的全局队列的优先级,可记为固定(优先级 DISPATCH_QUEUE_PRIORITY_DEFAULT);第二个是保留的参数,没什么用直接写0
NSLog(@"哈哈");
});
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
2.串行队列
//是创建得到的,不能直接获取只能同步
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
3.主队列 dispatch_get_main_queue(刷新界面)
//只能同歩
dispatch_async(dispatch_get_main_queue(),^{
NSLog(@"main - > %@", [NSThread currentThread]);
});
Demo:
//插座变量
@property (weak, nonatomic) IBOutlet UIImageView *baiduImageView;
//开始触摸事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//为避免循环引用;采取弱引用方式(遇到block大多可采用此方法)
__weak typeof(self)weakSelf=self;
//1.先创建一个全局并发队列
dispatch_queue_t queue=dispatch_get_global_queue(0, 0);
//2.调用异步函数
dispatch_async(queue, ^{
NSString *strUrl=[NSString stringWithFormat:@"https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png"];
NSURL *url=[NSURL URLWithString:strUrl];
NSData *data=[NSData dataWithContentsOfURL:url];
UIImage *image=[UIImage imageWithData:data];
//3.主队列刷新界面
dispatch_sync(dispatch_get_main_queue(), ^{
weakSelf.baiduImageView.image=image;
});
});
}
- 4.执行任务的方式:同步、异步
1.dispatch_sync 同步函数会发生堵塞;堵塞在同步函数以下的函数,即堵塞在接下来的函数操作(因为 FIFO 原则,按顺序执行)
- 5.GCD 通过信号量控制并发数
注意:NSoperation 可以直接设置并发数,就没有这么麻烦了
- 6.GCD:2个核心就是任务和队列
在线程池里取出队列,把任务添加放进队列里面进行执行
- 7.GCD:栅栏
场景:有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作;可以通过栅栏实现,也可以通过线程组来实现。
栅栏实现:这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务,需要用到dispatch_barrier_async方法在两个操作组间形成栅栏
- 8.使用 GCD 的时候如何让线程同步?目前就三种(7栅栏就是当中一种)
1.dispatch_group(线程组):
2.dispatch_barrier(栅栏)
3.dispatch_semaphore(信号量)实际应用:在开发中我们需要等待某个网络回调完之后才执行后面的操作,即上面说的同步。具体例子比如做通讯录的时候需要判断权限,才能获取通讯录
其他问法:如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
- 信号量特别说明(//www.greatytc.com/p/04ca5470f212)
有三个函数:
dispatch_semaphore_create 创建一个semaphore;(可以理解成信号总量)
dispatch_semaphore_signal 发送一个信号;(信号总量+1)
dispatch_semaphore_wait 等待信号;(当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1)注意:信号量为0则阻塞线程,大于0则不会阻塞。我们通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建信号量,并且设置值为10
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++)
{ // 由于是异步执行的,所以每次循环 Block 里面的 dispatch_semaphore_signal 根本还没有执行就会
//执行 dispatch_semaphore_wait,从而 semaphore-1。当循环10此后,semaphore 等于0,则会阻塞线程,直到
//执行了 Block 的 dispatch_semaphore_signal 才会继续执行
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
//每次发送信号则semaphore会+1,
dispatch_semaphore_signal(semaphore);
});
}
上面的注释已经把如何通过控制信号量来控制线程同步解释的比较浅显了。关键就是这句:// 由于是异步执行的,所以每次循环 Block 里面的 dispatch_semaphore_signal 根本还没有执行就会执行dispatch_semaphore_wait,从而 semaphore-1。当循环10此后,semaphore 等于0,则会阻塞线程,直到执行了 Block 的 dispatch_semaphore_signal 才会继续执行
其他
1.GCD 死锁
死锁就是线程相互等待造成;通俗的解释:面试官说只要你告诉我死锁就让你进公司,面试者说只要你让我进公司就告诉你死锁。
2.各种队列执行效果