iOS 多线程
先看一篇阮一峰写关于进程和线程的文章,快速了解线程的相关概念。
随着现在计算机硬件的发展,多核心、高频率的cpu越来越普及,为了充分发挥cpu的性能,在不通的环境下实现cpu的利用最大化,多线程技术在这个时候显得越发重要。同时,在程序中合理的使用多线程,可以让程序变得更加有效、靠谱。所以学习这一知识是一项有意义的事情。
iOS中,只有主线程跟Cocoa关联,也即是在这个线程中,更新UI总是有效的,如果在其他线程中更新UI有时候会成功,但可能失败。所以苹果要求开发者在主线程中更新UI。但是如果我们吧所有的操作都放置在主线程中执行,当遇到比较耗时的操作的时候,势必会阻塞线程,出现界面卡顿的情况。这时候采取将耗时的操作放入后台线程中操作,且保持主线程只更新UI是我们推荐的做法。
在iOS中,要实现多线程,一共有四种方式。 它们分别是:
- pthreads POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32[1]这篇文章不介绍
- NSThread 需要管理线程的生命周期、同步、加锁问题,这会导致一定的性能开销
- NSOperation & NSOperationQueue
- GCD iOS4开始,苹果发布了GCD,可以自动对线程进行管理。极大的方便了多线程的开发使用
备注:本文中相关的代码在https://github.com/lufubinGit/Multithreading。
一、pthreads
pthread是一套基于C的API,它不接受cocoa框架的控制:当手动创建pthread的时候,cocoa框架并不知道。 苹果不建议在cocoa中使用pthread,但是如果为了方便不得不使用,我们应该小心的使用。
下面这些方法可以创建pthread
OC pthread_attr_t qosAttribute; pthread_attr_init(&qosAttribute); pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0); pthread_create(&thread, &qosAttribute, f, NULL);
SWIFT var thread = pthread_t() var qosAttribute = pthread_attr_t() pthread_attr_init(&qosAttribute) pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_UTILITY, 0) pthread_create(&thread, &qosAttribute, f, nil)
并且,可以使用下面的API对一个pthread进行修改。
苹果的文档中,有一篇文档讲述了GCD中使用pthread的禁忌:Compatibility with POSIX Threads。
OBJECTIVE-C pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND,0); SWIFT pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0)
二、NSThread
对于NSThread,在使用的过程中,我们需要手动完成很多动作才能确保线程的顺利运行。但与此同时,它给我们带来更大的定制化空间。
1.创建NSThread。
对于NSThread的创建,苹果给出了三种使用方式。
detachNewThreadSelector(_:toTarget:with:) detachNewThreadSelector会创建一个新的线程,并直接进入线程执行。 initWith(Target:selector:object:) iOS10.0之前的创建方式,需要手动执行。 initWithBlock iOS10.0之后,可以创建一个执行block的线程。
2.NSThread线程通信。
如果我们想对已经存在的线程进行操作,可以使用
performSelector:onThread:withObject:waitUntilDone:
跳转到目标线程执行,实现线程间跳转,达到线程通信的目的。
但是需要注意的是,这个方法不适合频繁的进行通信,尤其是对于一些敏感的操作。
3.NSThread线程的状态。
在一个线程中,可以通过相关的函数获取到它的当前状态。
+ isMainThread:判断当前线程是不是主线程。 + mainThread:获取当前的主线程。 + isMultiThreaded :判断当前环境是不是多线程环境 + threadDictionary :获取包含项目中的线程的字典 @property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0); 是否处于运行状态 @property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0); 是否处于完成状态 @property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0); 是否处于取消状态
4.NSThread线程的优先级。
可通过给NSThread设置优先级。以便让开发者更灵活的控制程序的执行。
+ threadPriority Returns the current thread’s priority. 返回当前线程的优先级别 threadPriority The receiver’s priority 消息发送者的优先级,这个发送者是一个NSThread对象 + setThreadPriority: Sets the current thread’s priority. 设置线程的优先级
5.停止线程/终止线程
+ sleepUntilDate: Blocks the current thread until the time specified. 直到某时刻执行 + sleepForTimeInterval: Sleeps the thread for a given time interval. 暂停线程 + exit Terminates the current thread. 关闭线程,这里调用之前,为了确保程序的安全,我们应在明确线程的状态是isFinished 和 isCancelled的时候执行。 - cancel Changes the cancelled state of the receiver to indicate that it should exit. 主动进入取消状态,如果当时线程没有完成,会继续执行完成。
6.使用NSThread
- (void)viewDidLoad { [super viewDidLoad]; _testCount = 100; _t1 = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil]; _t1.name = @"线程一"; [_t1 start]; NSInvocationOperation } -(void)test{ for (int i = 0; i < 5 ; i++) { [NSThread sleepForTimeInterval:0.05]; NSLog(@"%ld,%@",(long)_testCount--,[[NSThread currentThread] name]); } }
三、NSOperation & NSOperationQueue
1.NSOperation
NSOperation是对于线程对象的抽象封装,不会被直接使用,在日常的开发中,会使用它的两个子类:NSInvocationOperation
和 NSBlockOperation。
NSInvocationOperation类是NSOperation的具体子类,用于管理指定为调用的单个封装任务的执行。 您可以使用此类来启动包含在指定对象上调用选择器的操作。 此类实现非并发操作。NSBlockOperation类也是NSOperation的具体子类,用于管理一个或多个block块的并发执行。 您可以使用此对象一次执行多个block,而无需为每个块创建单独的操作对象。 当执行多个程序段时,只有当所有程序段执行完毕时,才会将操作本身完成。
NSInvocationOperation实现非并发操作。
_invCationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test2:) object:nil]; _invCationOp.name = @"invocation线程"; [_invCationOp start];
打印:
<NSThread: 0x608000076fc0>{number = 1, name = main},
<NSThread: 0x608000076fc0>{number = 1, name = main}
从打印结果可以看出,NSInvocationOperation实现的是非并法的操作,至于在哪个线程中操作,取决于start的当前调用时的线程。
如果我们需要创建一个并发的Queue,可以使用NSBlockOperation。如果我们像这样创建:
- (void)blockOperation{ NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]); }]; [blockOp addExecutionBlock:^{ NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]); }]; [blockOp addExecutionBlock:^{ NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]); }]; [blockOp start]; }
打印:
2017-05-23 15:11:00.289 多线程[5377:589808] <NSThread: 0x60000006fec0>{number = 1, name = main},<NSThread: 0x60000006fec0>{number = 1, name = main}
2017-05-23 15:11:00.289 多线程[5377:589808] <NSThread: 0x60000006fec0>{number = 1, name = main},<NSThread: 0x60000006fec0>{number = 1, name = main}
2017-05-23 15:11:00.289 多线程[5377:589808] <NSThread: 0x60000006fec0>{number = 1, name = main},<NSThread: 0x60000006fec0>{number = 1, name = main}
显然,这里都在主线程中执行,不能证明NSBlockOperation具有并发的能力,这是因为,每个NSBlockOperation对象的会优先在主线程中执行。如果主线程受到阻塞的时候才会开辟另一个线程去执行其他的操作。比如向下面这样:
1 //NSBlockOperation 2 - (void)blockOperation{ 3 NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{ 4 NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]); 5 }]; 6 7 [blockOp addExecutionBlock:^{ 8 [NSThread sleepForTimeInterval:2.0]; 9 10 NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]); 11 }]; 12 [blockOp addExecutionBlock:^{ 13 [NSThread sleepForTimeInterval:2.0]; 14 NSLog(@"%@,%@",[NSThread currentThread],[NSThread mainThread]); 15 }]; 16 17 18 [blockOp start]; 19 }
打印:
2017-05-23 15:28:37.780 多线程[5645:617976] <NSThread: 0x60800006f700>{number = 1, name = main},<NSThread: 0x60800006f700>{number = 1, name = main}
2017-05-23 15:28:39.848 多线程[5645:618027] <NSThread: 0x60800007bf80>{number = 3, name = (null)},<NSThread: 0x60800006f700>{number = 1, name = (null)}
2017-05-23 15:28:39.848 多线程[5645:618028] <NSThread: 0x60800007bfc0>{number = 4, name = (null)},<NSThread: 0x60800006f700>{number = 1, name = (null)}=
这里就是异步执行了。 NSBlockOperation在使用的过程中,会针对主线程当前的使用情况,选择性的创建其他的线程。在提升流畅度的同时,还节约了资源。
2.NSOperationQueue
NSOperationQueue:手动管理异步执行。 如果我们想使用并发,并且要作到精确掌握并发的线程。可以使用NSOperationQueue。这是一个操作队列,如果将NSOperation的具体子类对象添加进来的时候,开启之后,所有的对象没有先后,会异步执行各自的代码。
- (void)operationQueue{ NSOperationQueue *queue = [[NSOperationQueue alloc] init]; NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"%ld,%@,%@",(long)_testCount--,[NSThread currentThread],[NSThread mainThread]); }]; NSInvocationOperation *invCationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test2:) object:nil]; invCationOp.name = @"invocation线程"; [queue addOperation:invCationOp]; [queue addOperation:blockOp]; }
打印:
2017-05-23 15:38:45.110 多线程[5772:633972] 100,<NSThread: 0x6080000773c0>{number = 3, name = (null)},<NSThread: 0x60000006bc80>{number = 1, name = (null)}
2017-05-23 15:38:45.203 多线程[5772:633975] 99,<NSThread: 0x608000078800>{number = 4, name = (null)},<NSThread: 0x60000006bc80>{number = 1, name = (null)}
在NSOperationQueue中,正常情况下,所有的operation的执行次序是随机,如果我们想要某个operation被率先执行,可以将这个operation的优先级调高。对于优先级有以下的选择:
[invCationOp setQueuePriority:NSOperationQueuePriorityVeryHigh];
对于优先级有以下的选择:
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) { NSOperationQueuePriorityVeryLow = -8L, 最低 NSOperationQueuePriorityLow = -4L, 次低 NSOperationQueuePriorityNormal = 0, 普通 不做任何操作的operation的优先级是这个 NSOperationQueuePriorityHigh = 4, 次高 NSOperationQueuePriorityVeryHigh = 8 最高 };
当然,如果有很多operation,使用的优先级不能满足的时候,还可以设置 operation的依赖关系。 设置依赖之后,将会先执行依赖对象。
[invCationOp addDependency:blockOp];
值得注意的是:优先级和依赖关系不是冲突的。 优先级的选择会在依赖关系下发生效果,也就是,在依赖关系成立的情况下,优先级的才会有效。
三、GCD - Grand Central Dispatch
Grand Central Dispatch早在Mac OS X 10.6雪豹上就已经存在了。后在iOS4.0的时候被引入。Grand Central Dispatch是OS X中的一个低级框架,用于管理整个操作系统中的并发和异步执行任务。本质上,随着处理器核心的可用性,任务排队等待执行。通过允许系统控制对任务的线程分配,GCD更有效地使用资源,这有助于系统和应用程序运行更快,高效和响应。
GCD的一个重要的对象是队列:Dispatch Queue。跟Operationqueue类似,通过将Operation加入到队列中,执行相应的单元。在GCD中,大量采用了block的形式创建类似的operation。
1. Dispatch Queue 创建
Dispatch Queue 分为两类,主要是根据并行和串行来区分:
a. Serial Dispatch Queue: 线性执行的线程队列,遵循FIFO(First In First Out)原则; 又叫private dispatch queues,同时只执行一个任务。Serial queue常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然各自是同步,但serial queue之间是并发执行。 main dipatch属于这个类别。
b. Concurrent Dispatch Queue: 并发执行的线程队列,并发执行的处理数取决于当前状态。又叫global dispatch queue,可以并发的执行多个任务,但执行完成顺序是随机的。系统提供四个全局并发队列,这四个队列有这对应的优先级,用户是不能够创建全局队列的,只能获取。
我们可以自定义队列,默认创建的队列是串行的,但是也可以指定创建一个并行的队列:
//串行队列 dispatch_queue_create("com.deafultqueue", 0)
//串行队列 dispatch_queue_create("com.serialqueue", DISPATCH_QUEUE_SERIAL) //并行队列 dispatch_queue_create("com.concurrentqueue", DISPATCH_QUEUE_CONCURRENT)
除了自定义队列,系统其实也为有一些已经公开的队列。这些队列不需要我们显示的创建,只能通过获取的方式得到:
dispatch_get_main_queue() 获取当前的APP主队列,这个队列在主线程中,通常我们调用它进行界面的刷新。
dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) 获取全局的Concurrent队列,这里苹果提供了四种不同的优先级,
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
也即时有四个不同的并行队列。
2. Dispatch Queue 执行
GCD的队列有串行和并行两种队列,同时我们可以同步和异步两种方式执行队列,所以最多有四种不同的场景。
(1)串行同步。 凡涉及到同步的的都会阻塞线程。 UI线程—也即是我们的所说的主线程默认情况下其实就是执行同步的。这个时候如果有一些耗时间的操作,则会出现卡顿的现象。这种方式大部分情况用于能快速响应和后台线程的耗时场景中。
//串行同步 dispatch_queue_t serialQ = dispatch_queue_create("串行", DISPATCH_QUEUE_SERIAL); //创建一个串行队列 NSLog(@"%@",[NSThread currentThread].description); dispatch_sync(serialQ, ^{ [NSThread sleepForTimeInterval:3]; NSLog(@"%@ -- %@队列",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]); }); dispatch_sync(serialQ, ^{ NSLog(@"%@ -- %@队列",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]); });
(2)串行异步。 这种情况下,GCD会开辟另一个新的线程,让队列中的内容在新的线程中按顺序执行。
//串行异步 dispatch_async(serialQ, ^{ NSLog(@"%@ 1-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]); }); dispatch_async(serialQ, ^{ NSLog(@"%@ 2-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]); }); dispatch_async(serialQ, ^{ NSLog(@"%@ 3-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]); }); dispatch_async(serialQ, ^{ NSLog(@"%@ 4-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(serialQ)]); });
(3)并行同步。 因为是同步执行,所以实际上这里的并行是没有意义的。 依然在当前的线程中按顺序执行,并阻塞。
dispatch_queue_t conCurrentQ = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT); //创建一个并行行队列 //并行同步 dispatch_sync(conCurrentQ, ^{ [NSThread sleepForTimeInterval:0.2]; NSLog(@"%@ 1-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); }); dispatch_sync(conCurrentQ, ^{ [NSThread sleepForTimeInterval:0.2]; NSLog(@"%@ 2-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); }); dispatch_sync(conCurrentQ, ^{ [NSThread sleepForTimeInterval:0.2]; NSLog(@"%@ 3-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); });
(4)并行异步。 并行异步将极大的利用资源。首先会开辟新的线程,并且,当所有线程备占用的情况下,会继续开辟(如果没有限制的话)。所以这里还涉及线程的最大值的问题。
//并行异步 dispatch_async(conCurrentQ, ^{ NSLog(@"%@ 1-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); }); dispatch_async(conCurrentQ, ^{ NSLog(@"%@ 2-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); }); dispatch_async(conCurrentQ, ^{ NSLog(@"%@ 3-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); }); dispatch_async(conCurrentQ, ^{ NSLog(@"%@ 4-- 队列:%@",[NSThread currentThread].description,[NSString stringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] ); });
3. Dispatch Queue 暂停和继续
我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;使用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。
4. Dispatch Queue 的销毁
每个队列在执行完添加到其中的所有的block事件的时候,在ARC模式下,会被自动销毁。 但是在手动管理内存的时候,我们需要调用
dispatch_release(queue);
来释放。
5.队列组 Dispatch Group (这些内容来自http://blog.csdn.net/q199109106q/article/details/8566300)
多数情况下,我们可能会遇到这种问题: 对一个页面中的多张图片,每张图片要单独的进行网络请求,我们没有办法保证每次的请求时间是一样的,但是项目经理说必须要在获取所有的图片的情况下,才可以进行对页面的刷新。这里有个很好例子可以解决这个问题。
// 根据url获取UIImage - (UIImage *)imageWithURLString:(NSString *)urlString { NSURL *url = [NSURL URLWithString:urlString]; NSData *data = [NSData dataWithContentsOfURL:url]; return [UIImage imageWithData:data]; } - (void)downloadImages { // 异步下载图片 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 下载第一张图片 NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg"; UIImage *image1 = [self imageWithURLString:url1]; // 下载第二张图片 NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg"; UIImage *image2 = [self imageWithURLString:url2]; // 回到主线程显示图片 dispatch_async(dispatch_get_main_queue(), ^{ self.imageView1.image = image1; self.imageView2.image = image2; }); }); }
但是我们发现,事实上,图片一和 二两者在请求的过程中是完全独立的, 但是这里明显的,图片一的下载将阻塞,直到下载完才会开始图片二的下载。这种方式毕竟还是有瑕疵的啊哈。
Dispatch Group可以帮助解决这个问题。 它是Dispatch Queue的组合,被加入到group的queue会在组内其他的queue也执行完操作的时候,有group统一调用预设好的一个block。最重要的是,在group中的内容是可以异步执行的。也即是多个队列在不同的线程执行。 如果图片大小差不多的话,这种方式将节省我们不一半的时间。 我们来看看这个模型。
//dispaach group dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue1, ^{ [NSThread sleepForTimeInterval:5.0]; NSLog(@"第一个项目执行完成。"); }); dispatch_group_async(group, queue2, ^{ [NSThread sleepForTimeInterval:10.0]; NSLog(@"第二个项目执行完成。"); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"集体回调"); });
6.GCD的其他的用法
(1)控制一段代码只执行一次。用在创建单例的时候再好不过了。
//控制代码只执行一次数 for(int i = 1 ;i <= 10 ;i++){ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLog(@"被执行 %d次",i); }); }
打印结果:2017-05-24 18:15:17.704 多线程[10542:898532] 被执行 1次
(2) 只能控制执行一次是不是有点不够完美 。dispatch_apply 可以让你控制一段代码执行任意多次。这里的执行是异步执行的,如果为了确保顺序执行,应该对执行的内容进行加锁。
//控制执行任意多次 dispatch_queue_t queueX = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); __block int count = 0; NSLock *lock = [[NSLock alloc]init]; dispatch_apply(5, queueX, ^(size_t index) { [lock lock]; NSLog(@"%d,%zu",count++,index); [lock unlock]; });
(3)做一个block式的延时。 除了使用performSelector之外,我们还以使用dispatch_after进行延时,并且是以block的形式进行。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"五秒钟之后执行的代码。"); });
相关参考: