UI进阶 多线程
一、多线程概述
- 程序、进程、线程
- 程序:由源代码生成的可执行应用。(例如:QQ.app)
- 进程:一个正在运行的程序可以看做一个进程。(例如:正在运行的QQ就是一个进程),进程拥有独立运行所需的全部资源。
- 线程:程序中独立运行的代码段。(例如:接收QQ消息的代码)
一个进程是由一或多个线程组成。进程只负责资源的调度和分配,线程才是程序真正的执行单元,负责代码的执行。
- 单线程
- 每个正在运行的程序(即进程),至少包含一个线程,这个线程叫主线程。
- 主线程在程序启动时被创建,用于执行main函数。
- 只有一个主线程的程序,称作单线程程序。
- 在单线程程序中,主线程负责执行程序的所有代码(UI展现以及刷新,网络请求,本地存储等等)。这些代码只能顺序执行,无法并发执行。
- 多线程
- 拥有多个线程的程序,称作多线程程序。
- iOS允许用户自己开辟新的线程,相对于主线程来讲,这些线程,称作子线程。
- 可以根据需要开辟若干子线程
- 子线程和主线程 都是 独立的运行单元,各自的执行互不影响,因此能够并发执行。
- 单线程、多线程区别
- 单线程程序:只有一个线程,即主线程,代码顺序执行,容易出现代码阻塞(页面假死)。
- 多线程程序:有多个线程,线程间独立运行,能有效的避免代码阻塞,并且提高程序的运行性能。
- 注意:iOS中关于UI的添加和刷新必须在主线程中操作。
二、iOS平台下的多线程
- iOS多线程实现种类
- NSThread
- NSOperationQueue
- NSObject
- GCD
三、NSThread
- NSThread是一个轻量级的多线程,它有以下两种创建方法
方法 功能 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 初始化一个子线程,但需要手动开启 + (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument 初始化一个子线程并自动开启 -start 开启子线程 -cancel 取消当前子线程,不是真正的取消,而是给子线程发送了一个“取消”消息,标记为canceled +exit 直接将线程退出
- 第一种:手动开辟一个子线程
Thread手动开辟一个子线程1 - (void)viewDidLoad { 2 #pragma mark - NSThread手动开辟子线程 3 // 参数1: target 4 // 参数2: 方法 5 // 参数3: 要传的参数 6 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(sayHello:) object:@"说你好!"]; 7 #pragma mark - 启动子线程 8 [thread start]; 9 10 // 判断一个线程是否正在执行 11 NSLog(@"是否正在执行%d",[thread isExecuting]); 12 // 判断一个线程是否完成了任务(是否执行完毕) 13 NSLog(@"是否执行完毕%d", [thread isFinished]); 14 15 #pragma mark - 释放子线程的两种方式 16 // 第一种:取消线程,不是真正的取消,而是给子线程发送了一个“取消”消息,标记为canceled 17 // 判断线城是否被标记为canceled 18 // NSLog(@"%d", [thread isCancelled]); 19 // [thread cancel]; 20 // // 第二种:直接将线程退出 21 // [NSThread exit]; 22 23 } 24 25 - (void)sayHello:(NSString *)hello { 26 NSLog(@"%@", hello); 27 // 打印当前线程 28 NSLog(@"当前线程:%@", [NSThread currentThread]); 29 NSLog(@"主线程:%@", [NSThread mainThread]); 30 }
开辟子线程成功 - 第一种:手动开辟一个子线程
-
-
第二种:
Thread自动开辟子线程1 - (void)viewDidLoad { 2 #pragma mark - NSThread自动开辟子线程 3 // 线程自动开启,不需要创建NSThread的对象 4 // 与手动开启线程的方法相比,target 和 selector 两个参数顺序颠倒 5 [NSThread detachNewThreadSelector:@selector(sayHello:) toTarget:self withObject:@"说你好!"]; 6 } 7 8 - (void)sayHello:(NSString *)hello { 9 NSLog(@"%@", hello); 10 // 打印当前线程 11 NSLog(@"当前线程:%@", [NSThread currentThread]); 12 NSLog(@"主线程:%@", [NSThread mainThread]); 13 }
-
- 开辟子线程成功
- 其他方法
- 注意:
- 每个线程都维护着与自己对应的NSAutoreleasePool对象,将其放在线程栈的栈顶。当线程结束时,会清空自动释放池。
- 为保证对象的及时释放,在多线程方法中需要添加自动释放池。
- 在应用程序打开的时候,系统会自动为主线程创建一个自动释放池。
- 我们手动创建的子线程需要我们手动添加自动释放池。
四、NSObject实现异步后台执行
1 - (void)viewDidLoad { 2 #pragma mark - NSObject开辟子线程 3 /** 4 * 开启线程的方式之二:NSObject 5 */ 6 // 使用performSelectorInBackground开辟子线程 7 // 参数1:方法 8 // 参数2:要传的参数 9 [self performSelectorInBackground:@selector(sayHello:) withObject:@"说你好!"]; 10 self.view.backgroundColor = [UIColor yellowColor]; 11 12 } 13 14 - (void)sayHello:(NSString *)hello { 15 NSLog(@"%@", hello); 16 // 打印当前线程 17 NSLog(@"当前线程:%@", [NSThread currentThread]); 18 NSLog(@"主线程:%@", [NSThread mainThread]); 19 20 // 回到主线程修改view的背景颜色 21 // 参数1:方法 22 // 参数2:要传的参数 23 // 参数3:是否等待子线程完成后再进入主线程 24 [self performSelectorOnMainThread:@selector(changeColor) withObject:nil waitUntilDone:NO]; 25 } 26 27 - (void)changeColor { 28 self.view.backgroundColor = [UIColor cyanColor]; 29 // 打印当前线程 30 NSLog(@"==当前线程:%@", [NSThread currentThread]); 31 NSLog(@"==主线程:%@", [NSThread mainThread]); 32 }
开辟子线程成功
NSThread和NSObject结合使用:同步请求延迟加载图片
1 @interface ViewController () 2 /// 声明一个ImageView用来显示图片 3 @property (nonatomic, strong) UIImageView *showImageView; 4 /// 声明一个NSData类型的数据用于接收图片的数据 5 @property (nonatomic, strong) NSData *imageData; 6 @end 7 8 @implementation ViewController 9 10 - (void)viewDidLoad { 11 [super viewDidLoad]; 12 // 创建imageView 13 _showImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 100, 414, 300)]; 14 _showImageView.backgroundColor = [UIColor greenColor]; 15 [self.view addSubview:_showImageView]; 16 17 18 } 19 20 #pragma mark - 触发子线程去加载数据 21 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 22 [self performSelectorInBackground:@selector(loadImageData) withObject:nil]; 23 } 24 25 // 加载图片数据 26 - (void)loadImageData { 27 NSLog(@"当前线程:%@", [NSThread currentThread]); 28 NSLog(@"主线程:%@", [NSThread mainThread]); 29 NSURL *url = [NSURL URLWithString:@"http://img4q.duitang.com/uploads/item/201506/13/20150613185209_nHy5E.jpeg"]; 30 NSURLRequest *request = [NSURLRequest requestWithURL:url]; 31 self.imageData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil]; 32 // 创建主线程用于刷新UI 33 [self performSelectorOnMainThread:@selector(renovateUI) withObject:nil waitUntilDone:YES]; 34 35 } 36 37 // 主线程刷新UI 38 - (void)renovateUI { 39 // 安全判断 40 if ([NSThread mainThread]) { 41 _showImageView.image = [UIImage imageWithData:self.imageData]; 42 } 43 } 44 @end
点击触发事件,开辟子线程成功,在主线程中刷新UI,图片显示成功
五、NSOperation和NSOperationQueue
- NSOperation
- NSOperation类,在MVC中属于M,是用来封装单个任务相关的代码和数据的抽象类
- 因为它是抽象的,不能够直接使用这个类,而是使用子类( NSInvocationOperation或NSBlockOperation )来执行实际任务。
- NSOperation(含子类),只是一个操作,本身无主线程、子线程之分,可在任意线程中使用。通常与NSOperationQueue结合使用。
- NSInvocationOperation
- NSInvocationOperation是NSOperation的子类
- 封装了执行操作的target和要执行的action。
1 - (void)viewDidLoad { 2 /** 3 * NSOperation不能直接进行多线程的创建,需要借助:NSOperationQueue。 4 NSOperation的子类本身和多线程没有任何关系,它只是封装了一定的代码段和数据去实现一个功能。 5 在单独使用NSOperation的子类去创建线程的时候,实际上线程没有真正被创建。 6 */ 7 // 使用NSOperation的第一个子类去创建子线程 -- NSInvocationOperation 8 NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(sayHello:) object:@"😊"]; 9 // 在单独使用NSOperation的子类去创建线程的时候,一定要启动 10 [operation start]; 11 12 } 13 14 - (void)sayHello:(NSString *)str { 15 NSLog(@"%@", str); 16 // 此时发现两个线程地址相同,也就是当前线程就是主线程,子线程没有创建成功 17 // 18 NSLog(@"%@", [NSThread currentThread]); 19 NSLog(@"%@", [NSThread mainThread]); 20 }
没有开辟子线程
- NSBlockOperation
- NSBlockOperation是NSOperation的子类
- 封装了需要执行的代码块
1 - (void)viewDidLoad { 2 /** 3 * NSOperation不能直接进行多线程的创建,需要借助:NSOperationQueue。 4 NSOperation的子类本身和多线程没有任何关系,它只是封装了一定的代码段和数据去实现一个功能。 5 在单独使用NSOperation的子类去创建线程的时候,实际上线程没有真正被创建。 6 */ 7 // 使用NSOperation的第二个子类去创建子线程 -- NSBlockOperation 8 NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ 9 NSLog(@"我是Block"); 10 // 此时发现两个线程地址相同,也就是当前线程就是主线程,子线程没有创建成功 11 // 12 NSLog(@"++++++%@", [NSThread currentThread]); 13 NSLog(@"++++++%@", [NSThread mainThread]); 14 }]; 15 // 在单独使用NSOperation的子类去创建线程的时候,一定要启动 16 [blockOperation start]; 17 }
没有开辟子线程
- NSOperationQueue
- NSOperationQueue是操作队列,他用来管理一组Operation对象的执行,会根据需要自动为Operation开辟合适数量的线程,以完成任务的并行执行
- 其中NSOperation可以调节它在队列中的优先级(使用addDependency:设置依赖关系)
- 当最大并发数设置为1的时候,能实现线程同步(串行执行)
注意:一旦将创建完成的线程放到队列中,就不能再start启动,否则程序会crush
1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 /** 4 * NSOperation不能直接进行多线程的创建,需要借助:NSOperationQueue。 5 NSOperation的子类本身和多线程没有任何关系,它只是封装了一定的代码段和数据去实现一个功能。 6 在单独使用NSOperation的子类去创建线程的时候,实际上线程没有真正被创建。 7 */ 8 // 创建操作队列 9 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 10 // 最大并发数量 11 // 当值设置为1的时候,可以叫做串行:即顺序执行 12 // 当值设置大于1的时候,叫做并行:多条通道同时进行各自的任务 13 queue.maxConcurrentOperationCount = 2; 14 for (int i = 0; i < 10; i++) { 15 NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{ 16 NSLog(@"%@, %@, %d", [NSThread currentThread], [NSThread mainThread], i); 17 }]; 18 #warning - 注意:一旦将创建完成的线程放到队列中,就不能再start启动,否则程序会crush 19 [queue addOperation:blockOperation]; 20 } 21 }
开辟子线程成功
六、GCD(Grand Central Dispatch)使用
- GCD简介
- Grand Central Dispatch (GCD)是Apple开发的一种多核编程技术。主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。
- GCD纯C代码封装,提供函数实现多线程开发,性能更高,功能也更加强大。
- 它首次发布在Mac OS X 10.6 ,iOS 4及以上也可用
- GCD的优势
- GCD是苹果公司为多核的并行运算提出的解决方案
- GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
- GCD核心概念
- 任务:具有一定功能的代码段。一般是一个block或者函数
- 分发队列:GCD以队列的方式进行工作,FIFO
- GCD会根据分发队列的类型,创建合适数量的线程执行队列中的任务
- GCD中两种队列
- SerialQueue:一次只执行一个任务。Serial queue通常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然它们各自是同步执行的,但Serial queue与Serial queue之间是并发执行的。SerialQueue能实现线程同步
- Concurrent:可以并发地执行多个任务,但是遵守FIFO
- 创建串行队列
- 使用主队列(跟主线程相关联的队列)
主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行
使用主队列1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 使用GCD去创建串行队列 4 // 第一种:系统提供的创建串行队列的方法 5 // 使用主队列(跟主线程相关联的队列) 6 // 主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行 7 dispatch_queue_t queue = dispatch_get_main_queue(); 8 // 往队列里面添加任务 9 dispatch_async(queue, ^{ 10 NSLog(@"这是第一个任务,当前线程是:%@, 是否主线程 :%d ", [NSThread currentThread], [[NSThread currentThread] isMainThread]); 11 }); 12 13 }
所有任务都在主线程中进行
- 使用主队列(跟主线程相关联的队列)
- 创建并行队列
- GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建
- 四个优先级
- DISPATCH_QUEUE_PRIORITY_DEFAUL, 默认(中)
- DISPATCH_QUEUE_PRIORITY_HIGH, 高
- DISPATCH_QUEUE_PRIORITY_LOW, 低
- DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台
全局并发队列1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 使用GCD创建并行队列 4 // 第一种方式 5 // global queue GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建 6 // 优先级 7 // DISPATCH_QUEUE_PRIORITY_DEFAUL, 默认(中) 8 // DISPATCH_QUEUE_PRIORITY_HIGH, 高 9 // DISPATCH_QUEUE_PRIORITY_LOW, 低 10 // DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台 11 // 第一个参数就是队列的优先级,第二个参数是苹果预留的参数为了以后去使用,目前没有用到,填写0 12 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 13 #pragma mark - 向队列中添加任务 14 dispatch_async(queue, ^{ 15 NSLog(@"current1%@, main1%@", [NSThread currentThread], [NSThread mainThread]); 16 }); 17 dispatch_async(queue, ^{ 18 NSLog(@"current2%@, main2%@", [NSThread currentThread], [NSThread mainThread]); 19 }); 20 dispatch_async(queue, ^{ 21 NSLog(@"current3%@, main3%@", [NSThread currentThread], [NSThread mainThread]); 22 }); 23 dispatch_async(queue, ^{ 24 NSLog(@"current4%@, main4%@", [NSThread currentThread], [NSThread mainThread]); 25 }); 26 dispatch_async(queue, ^{ 27 NSLog(@"current5%@, main5%@", [NSThread currentThread], [NSThread mainThread]); 28 }); 29 }
开辟子线程成功,子线程并发执行
- 手动创建串行/并行队列
- 自己创建的队列,第一个参数是队列的名字,第二个参数队列的类型(串行队列、并行队列),这种方式创建的队列,它会自己去开辟一个子线程去完成队列里面的任务
- 串行队列
手动创建串行队列1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 第二种创建串行队列的方式 4 // 参数1:系统的保留字段,队列名 5 // 参数2:系统提供的宏 6 // 两个参数的位置可以互换,没有严格限制 7 dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_SERIAL); 8 9 #pragma mark - 向队列中添加任务 10 dispatch_async(queue, ^{ 11 NSLog(@"current1%@, main1%@", [NSThread currentThread], [NSThread mainThread]); 12 }); 13 dispatch_async(queue, ^{ 14 NSLog(@"current2%@, main2%@", [NSThread currentThread], [NSThread mainThread]); 15 }); 16 dispatch_async(queue, ^{ 17 NSLog(@"current3%@, main3%@", [NSThread currentThread], [NSThread mainThread]); 18 }); 19 dispatch_async(queue, ^{ 20 NSLog(@"current4%@, main4%@", [NSThread currentThread], [NSThread mainThread]); 21 }); 22 dispatch_async(queue, ^{ 23 NSLog(@"current5%@, main5%@", [NSThread currentThread], [NSThread mainThread]); 24 }); 25 }
开辟了一个子线程,任务顺序执行
- 并行队列
手动创建并行队列1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 第二种创建并行队列的方式 4 // 参数1:系统保留字段,队列名 5 // 参数2:系统提供的宏 6 dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT); 7 8 #pragma mark - 向队列中添加任务 9 dispatch_async(queue, ^{ 10 NSLog(@"current1%@, main1%@", [NSThread currentThread], [NSThread mainThread]); 11 }); 12 dispatch_async(queue, ^{ 13 NSLog(@"current2%@, main2%@", [NSThread currentThread], [NSThread mainThread]); 14 }); 15 dispatch_async(queue, ^{ 16 NSLog(@"current3%@, main3%@", [NSThread currentThread], [NSThread mainThread]); 17 }); 18 dispatch_async(queue, ^{ 19 NSLog(@"current4%@, main4%@", [NSThread currentThread], [NSThread mainThread]); 20 }); 21 dispatch_async(queue, ^{ 22 NSLog(@"current5%@, main5%@", [NSThread currentThread], [NSThread mainThread]); 23 }); 24 }
开辟了多个子线程,任务无序执行
- 串行队列
- 自己创建的队列,第一个参数是队列的名字,第二个参数队列的类型(串行队列、并行队列),这种方式创建的队列,它会自己去开辟一个子线程去完成队列里面的任务
- 系统保留字段(队列名)在断点调试时可以用来辨别当前执行任务的是哪个队列
- GCD功能
- dispatch_async() 异步函数,往队列中添加任务,任务会排队执行
- dispatch_after() 往队列中添加任务,任务不但会排队,还会在延迟的时间点执行
- dispatch_apply() 往队列中添加任务,任务会重复执行n次
- 延迟 dispatch_after()
延迟 dispatch_after1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 延迟 4 // 参数1:时间 5 // 参数1.1:DISPATCH_TIME_NOW 表示当前 6 // DISPATCH_TIME_FOREVER 表示遥远的未来; 7 // 参数1.2:距离参数1有多长时间 8 // 参数2:队列 9 // 参数3:block 10 11 dispatch_async(dispatch_get_main_queue(), ^{ 12 NSLog(@"视图加载完成"); 13 }); 14 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 15 NSLog(@"3.0秒之后"); 16 }); 17 18 }
延迟成功
- dispatch_apply() 重复添加任务
重复添加任务 dispatch_apply()1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 重复向一个队列中添加多个任务 4 dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_CONCURRENT); 5 // 重复向队列中添加任务 6 // block要随便添加一个参数名 7 dispatch_apply(10, queue, ^(size_t index) { 8 NSLog(@"%ld, %@", index, [NSThread currentThread]); 9 }); 10 }
添加成功
- 队列组
- dispatch_group_async() 将任务添加到队列中,并添加分组标记
- dispatch_group_notify() 监听,当某个分组的所有任务执行完之后,此任务才会执行
要实现多个任务并行执行,所有任务执行完毕之后执行某个操作时,建议用队列组
1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 #pragma mark - 分组 4 // 创建一个分组 5 dispatch_group_t group = dispatch_group_create(); 6 // 创建一个并行队列 7 dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_CONCURRENT); 8 // 创建任务 9 dispatch_group_async(group, queue, ^{ 10 NSLog(@"任务1"); 11 }); 12 dispatch_group_async(group, queue, ^{ 13 NSLog(@"任务2"); 14 }); 15 dispatch_group_async(group, queue, ^{ 16 NSLog(@"任务3"); 17 }); 18 // 用于监听所有任务的执行情况 19 // 代码必须不能写在第一个任务之前,否则不能监听全部任务, 20 dispatch_group_notify(group, queue, ^{ 21 NSLog(@"完事儿了"); 22 }); 23 24 }
监听成功
- dispatch_barrier_async() 将任务添加到队列中,此任务执行的时候,其他任务停止执行
例如数据库的读取可以并行执行,但数据库的写入需要串行执行,此时将写入数据库的任务通过dispatch_barrier_async() 函数添加到队列中,实现串行执行。 - dispatch_once() 任务添加到队列中,但任务在程序运行过程中,只执行一次
该函数接收一个dispatch_once用于检查该代码块是否已经被调度的谓词(是一个长整型,实际上作为BOOL使用)。它还接收一个希望在应用的生命周期内仅被调度一次的代码块。
dispatch_once不仅意味着代码仅会被运行一次,而且还是线程安全的,这就意味着你不需要使用诸如@synchronized之类的来防止使用多个线程或者队列时不同步的问题。
此函数可以用来完善单例模式中的单例方法
单例方法1 #import "MyHandle.h" 2 3 4 static MyHandle *myHandle = nil; 5 @implementation MyHandle 6 7 #pragma mark - 传统单例写法 8 // 此时如果多个任务并发执行,就不会只初始化一次 9 //+ (instancetype)sharedMyHandle { 10 // if (myHandle == nil) { 11 // myHandle = [[MyHandle alloc] init]; 12 // } 13 // return myHandle; 14 //} 15 16 #pragma mark - 完整的单例写法 17 + (instancetype)sharedMyHandle { 18 // 保证在GCD多线程情况下只执行一次 19 static dispatch_once_t onceToken; 20 dispatch_once(&onceToken, ^{ 21 myHandle = [[MyHandle alloc] init]; 22 }); 23 return myHandle; 24 } 25 @end
- dispatch_sync() 同步函数,将任务添加到队列中,block不执行完,下面代码不会执行,与dispatch_async()异步函数对应记忆
async 不等 block 体执行完,就去执行下面的代码
sync会等待 block 体执行完成之后,才会去执行 block 体外面的代码 - dispatch_async_f () 将任务添加到队列中,任务是函数非block
dispatch_async_f()1 // 函数 2 void function(void * str){ 3 NSLog(@"这是一个函数,%s",str); 4 } 5 6 - (void)viewDidLoad { 7 [super viewDidLoad]; 8 // 第一个参数:队列 9 // 第二个参数:函数参数的内容,注意不要写成OC中的字符串 10 // 第三个参数:函数 11 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 12 dispatch_async_f(queue, "passValue", function); 13 14 }
七、线程间通信
- 线程间通信分为两种:
- 主线程进入子线程(前面的方法都可以)
- 子线程回到主线程
- 返回主线程的方法
注:dispatch_get_main_queue()在前面介绍GCD创建串行队列--主队列时有提到,此处不再赘述
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait 在前面NSThread和NSObject结合使用:同步请求延迟加载图片时有提到,此处不再赘述
八、线程互斥
多线程并行编程中,线程间同步与互斥是一个很有技巧的也很容易出错的地方。
线程间互斥应对的是这种场景:
多个线程操作同一个资源(即某个对象),需要保证线程在对资源的状态(即对象的成员变量)进行一些非原子性操作后,状态仍然正确。
典型的例子是“售票厅售票应用”。售票厅剩余20张票,10个窗口去卖这些票。这10个窗口,就是10条线程,售票厅就是他们共同操作的资源,其中剩余的20张票就是这个资源的一个状态。线程买票的过程就是去递减这个剩余数量的过程。
我们看看会发生什么问题
1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 // 模拟买票系统 4 // 一共20张票,10个窗口卖 5 __block NSInteger count = 20; 6 dispatch_queue_t ticketQueue = dispatch_queue_create("sell ticket", DISPATCH_QUEUE_CONCURRENT); 7 for (int i = 0; i < 10; i ++) { 8 dispatch_async(ticketQueue, ^{ 9 //这里相当于每个窗口卖2张票 10 for (int i = 0; i < 2; i ++) { 11 NSLog(@"买到了第%ld张票",count); 12 count--; 13 } 14 }); 15 } 16 }
不同“售票窗口”贩卖了同一张“票”
- 线程互斥解决方案
- 方法一 @synchronized 自动对参数对象加锁,保证临界区内的代码线程安全
- 方法二 NSLock
- 方法三 NSConditionLock 条件锁 可以设置条件
- 方法四 NSRecursiveLock 递归锁 多次调用不会阻塞已获取该锁的线程
- @synchronized, 代表这个方法加锁, 相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程例如B正在用这个方法,有的话要等正在使用synchronized方法 的线程B运行完这个方法后再运行此线程A,没有的话,直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
@synchronized 方法控制对类(一般在IOS中用在单例中)的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法锁方能执行,否则所属就会发生线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类,至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突(只要所有可能访问类的方法均被声明为 synchronized)
解决了线程互斥问题
@synchronized1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 // 模拟买票系统 4 // 一共20张票,10个窗口卖 5 __block NSInteger count = 20; 6 __weak typeof(self) weakSelf = self; 7 dispatch_queue_t ticketQueue = dispatch_queue_create("sell ticket", DISPATCH_QUEUE_CONCURRENT); 8 for (int i = 0; i < 10; i ++) { 9 dispatch_async(ticketQueue, ^{ 10 // 给买票操作加锁,保证代码块只有一个线程访问 11 @synchronized(weakSelf) { 12 //这里相当于每个窗口卖2张票 13 for (int i = 0; i < 2; i ++) { 14 NSLog(@"买到了第%ld张票",count); 15 count--; 16 } 17 } 18 19 }); 20 } 21 }
- NSLock
NSLock1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 // 模拟买票系统 4 // 一共20张票,10个窗口卖 5 __block NSInteger count = 20; 6 7 // 创建线程锁 8 NSLock *lock = [[NSLock alloc]init]; 9 dispatch_queue_t ticketQueue = dispatch_queue_create("sell ticket", DISPATCH_QUEUE_CONCURRENT); 10 for (int i = 0; i < 10; i ++) { 11 dispatch_async(ticketQueue, ^{ 12 // 给买票操作加锁,保证代码块只有一个线程访问 13 // 加锁,不会出现多个窗口同时卖一张票的情况 14 [lock lock]; 15 //这里相当于每个窗口卖2张票 16 for (int i = 0; i < 2; i ++) { 17 NSLog(@"买到了第%ld张票",count); 18 count--; 19 } 20 // 解锁 21 [lock unlock]; 22 }); 23 } 24 }
总结:
- NSThread、NSOperationQueue、NSObject、GCD都能实现多线程GCD是苹果提供的性能更高的方式
- 区别
- Thread:
是这几种方式里面相对轻量级的,但也是使用起来最负责的,你需要自己管理thread的生命周期,线程之间的同步。线程共享同一应用程序的部分内存空间, 它们拥有对数据相同的访问权限。你得协调多个线程对同一数据的访问,一般做法是在访问之前加锁,这会导致一定的性能开销。在 iOS 中我们可以使用多种形式的 thread。 - Cocoa threads:(NSObject)
使用NSThread 或直接从 NSObject 的类方法 performSelectorInBackground:withObject: 来创建一个线程。如果你选择thread来实现多线程,那么 NSThread 就是官方推荐优先选用的方式。 - Cocoa operations是基于 Obective-C实现的,类 NSOperation 以面向对象的方式封装了用户需要执行的操作,我们只要聚焦于我们需要做的事情,而不必太操心线程的管理,同步等事情,因为NSOperation已经为我 们封装了这些事情。 NSOperation 是一个抽象基类,我们必须使用它的子类。iOS 提供了两种默认实现:NSInvocationOperation 和 NSBlockOperation。但当单独使用NSOperation的子类去创建线程的时候,线程没有被真正的创建,需要借助NSOperationQueue类,当子线程被添加到NSOperationQueue类的对象中时,线程才真正创建成功,这个时候不要让子线程去调用start方法,否组程序会crush
- Grand Central Dispatch (GCD):
iOS4 开始支持,纯C语言封装,提供了很多非常强大的函数。GCD是苹果公司为多核的并行运算提出的解决方案,会自动利用更多的CPU内核(比如双核、四核),会自动管理线程的生命周期(创建线程、调度任务、销毁线程),程序员只需告诉GCD想执行什么任务,不需要编写任何线程管理代码。
- Thread:
- 区别
- 使用多线程开发的优点 :资源利用率更好; 程序设计在某些情况下更简单;程序响应更快;
- 多线程的缺点:多线程尽管提升了性能,但是存在一些访问限制,比如线程同步、线程互斥等;多线程在使用的时候,最终是要回到主线程刷新UI的,如果开辟过多的多线程,会造成CPU的消耗。
- 同步和异步的区别
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 串行和并行的区别
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
- 各种队列的执行效果
- 同步函数不具备开启线程的能力,无论是什么队列都不会开启线程;异步函数具备开启线程的能力,开启几条线程由队列决定(串行队列只会开启一条新的线程,并发队列会开启多条线程)。
- 同步函数
(1)并发队列:不会开线程
(2)串行队列:不会开线程 - 异步函数
(1)并发队列:能开启N条线程
(2)串行队列:开启1条线程
- 同步函数
- 同步函数不具备开启线程的能力,无论是什么队列都不会开启线程;异步函数具备开启线程的能力,开启几条线程由队列决定(串行队列只会开启一条新的线程,并发队列会开启多条线程)。


浙公网安备 33010602011771号