iOS多线程的使用

iOS中,只有主线程跟Cocoa关联,也即是在这个线程中,更新UI总是有效的,如果在其他线程中更新UI有时候会成功,但可能失败。所以苹果要求开发者在主线程中更新UI。但是如果我们吧所有的操作都放置在主线程中执行,当遇到比较耗时的操作的时候,势必会阻塞线程,出现界面卡顿的情况。这时候采取将耗时的操作放入后台线程中操作,且保持主线程只更新UI是我们推荐的做法。

在iOS中,要实现多线程,一共有四种方式。  它们分别是:

pthreadsPOSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。Windows操作系统也有其移植版pthreads-win32[1]这篇文章不介绍

NSThread需要管理线程的生命周期、同步、加锁问题,这会导致一定的性能开销

NSOperation & NSOperationQueue

GCDiOS4开始,苹果发布了GCD,可以自动对线程进行管理。极大的方便了多线程的开发使用

一、pthread

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

varthread= 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。

6OBJECTIVE-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 executingNS_AVAILABLE(10_5, 2_0);     是否处于运行状态

@property(readonly, getter=isFinished)BOOL finishedNS_AVAILABLE(10_5, 2_0);          是否处于完成状态

@property(readonly, getter=isCancelled)BOOL cancelledNS_AVAILABLE(10_5, 2_0);      是否处于取消状态

4.NSThread线程的优先级。

可通过给NSThread设置优先级。以便让开发者更灵活的控制程序的执行。

+ threadPriority  Returns the currentthread’s priority. 返回当前线程的优先级别

threadPriority        The receiver’s priority  消息发送者的优先级,这个发送者是一个NSThread对象

+ setThreadPriority:     Sets the currentthread’s priority. 设置线程的优先级

5.停止线程/终止线程

+ sleepUntilDate:   Blocks the currentthreaduntil the time specified.  直到某时刻执行

+ sleepForTimeInterval:     Sleeps thethreadfora given time interval.   暂停线程

+ exit    Terminates the currentthread.  关闭线程,这里调用之前,为了确保程序的安全,我们应在明确线程的状态是isFinished 和 isCancelled的时候执行。

- cancel    Changes the cancelled state of the receiver to indicate that it should exit.  主动进入取消状态,如果当时线程没有完成,会继续执行完成。

6.使用NSThread

- (void)viewDidLoad {

[superviewDidLoad];

_testCount = 100;

_t1 =  [[NSThread alloc] initWithTarget:selfselector:@selector(test) object:nil];

_t1.name = @"线程一";

[_t1 start];

NSInvocationOperation

}

-(void)test{

for(inti = 0; i < 5 ; i++) {

[NSThreadsleepForTimeInterval:0.05];

NSLog(@"%ld,%@",(long)_testCount--,[[NSThreadcurrentThread] name]);

}

}

三、NSOperation & NSOperationQueue

1.NSOperation

NSOperation是对于线程对象的抽象封装,不会被直接使用,在日常的开发中,会使用它的两个子类:NSInvocationOperation和NSBlockOperation。NSInvocationOperation类是NSOperation的具体子类,用于管理指定为调用的单个封装任务的执行。 您可以使用此类来启动包含在指定对象上调用选择器的操作。 此类实现非并发操作。NSBlockOperation类也是NSOperation的具体子类,用于管理一个或多个block块的并发执行。 您可以使用此对象一次执行多个block,而无需为每个块创建单独的操作对象。 当执行多个程序段时,只有当所有程序段执行完毕时,才会将操作本身完成。

NSInvocationOperation实现非并发操作。

3_invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];

_invCationOp.name = @"invocation线程";

[_invCationOp start];

打印:

{number = 1, name = main},

{number = 1, name = main}

从打印结果可以看出,NSInvocationOperation实现的是非并法的操作,至于在哪个线程中操作,取决于start的当前调用时的线程。

如果我们需要创建一个并发的Queue,可以使用NSBlockOperation。如果我们像这样创建:

- (void)blockOperation{

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp addExecutionBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp addExecutionBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

}];

[blockOp start];

}

打印:

2017-05-23 15:11:00.289多线程[5377:589808] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:11:00.289多线程[5377:589808] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:11:00.289多线程[5377:589808] {number = 1, name = main},{number = 1, name =  main}

显然,这里都在主线程中执行,不能证明NSBlockOperation具有并发的能力,这是因为,每个NSBlockOperation对象的会优先在主线程中执行。如果主线程受到阻塞的时候才会开辟另一个线程去执行其他的操作。比如向下面这样:

//NSBlockOperation

 - (void)blockOperation{

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

     }];

     [blockOp addExecutionBlock:^{

         [NSThreadsleepForTimeInterval:2.0];

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

     }];

     [blockOp addExecutionBlock:^{

         [NSThreadsleepForTimeInterval:2.0];

NSLog(@"%@,%@",[NSThreadcurrentThread],[NSThreadmainThread]);

     }];

     [blockOp start];

 }

打印:

2017-05-23 15:28:37.780多线程[5645:617976] {number = 1, name = main},{number = 1, name = main}

2017-05-23 15:28:39.848多线程[5645:618027] {number = 3, name = (null)},{number = 1, name = (null)}

2017-05-23 15:28:39.848多线程[5645:618028] {number = 4, name = (null)},{number = 1, name = (null)}=

这里就是异步执行了。NSBlockOperation在使用的过程中,会针对主线程当前的使用情况,选择性的创建其他的线程。在提升流畅度的同时,还节约了资源。

2.NSOperationQueue

NSOperationQueue:手动管理异步执行。  如果我们想使用并发,并且要作到精确掌握并发的线程。可以使用NSOperationQueue。这是一个操作队列,如果将NSOperation的具体子类对象添加进来的时候,开启之后,所有的对象没有先后,会异步执行各自的代码。

- (void)operationQueue{

NSOperationQueue*queue = [[NSOperationQueuealloc] init];

NSBlockOperation*blockOp = [NSBlockOperationblockOperationWithBlock:^{

NSLog(@"%ld,%@,%@",(long)_testCount--,[NSThreadcurrentThread],[NSThreadmainThread]);

}];

NSInvocationOperation*invCationOp = [[NSInvocationOperationalloc] initWithTarget:selfselector:@selector(test2:) object:nil];

invCationOp.name = @"invocation线程";

[queue addOperation:invCationOp];

[queue addOperation:blockOp];

}

打印:

2017-05-23 15:38:45.110多线程[5772:633972] 100,{number = 3, name = (null)},{number = 1, name = (null)}

2017-05-23 15:38:45.203多线程[5772:633975] 99,{number = 4, name = (null)},{number = 1, name = (null)}

在NSOperationQueue中,正常情况下,所有的operation的执行次序是随机,如果我们想要某个operation被率先执行,可以将这个operation的优先级调高。对于优先级有以下的选择:

[invCationOp setQueuePriority:NSOperationQueuePriorityVeryHigh];

对于优先级有以下的选择:

typedefNS_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(@"%@",[NSThreadcurrentThread].description);

dispatch_sync(serialQ, ^{

[NSThreadsleepForTimeInterval:3];

NSLog(@"%@ --  %@队列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_sync(serialQ, ^{

NSLog(@"%@ --  %@队列",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

(2)串行异步。 这种情况下,GCD会开辟另一个新的线程,让队列中的内容在新的线程中按顺序执行。

//串行异步

dispatch_async(serialQ, ^{

NSLog(@"%@ 1-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 2-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 3-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

dispatch_async(serialQ, ^{

NSLog(@"%@ 4-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(serialQ)]);

});

(3)并行同步。 因为是同步执行,所以实际上这里的并行是没有意义的。 依然在当前的线程中按顺序执行,并阻塞。

dispatch_queue_t  conCurrentQ = dispatch_queue_create("并行", DISPATCH_QUEUE_CONCURRENT);//创建一个并行行队列

//并行同步

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 1-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 2-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_sync(conCurrentQ, ^{

[NSThreadsleepForTimeInterval:0.2];

NSLog(@"%@ 3-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

(4)并行异步。 并行异步将极大的利用资源。首先会开辟新的线程,并且,当所有线程备占用的情况下,会继续开辟(如果没有限制的话)。所以这里还涉及线程的最大值的问题。

//并行异步

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 1-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 2-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 3-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String:dispatch_queue_get_label(conCurrentQ)] );

});

dispatch_async(conCurrentQ, ^{

NSLog(@"%@ 4-- 队列:%@",[NSThreadcurrentThread].description,[NSStringstringWithUTF8String: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 = [NSURLURLWithString:urlString];

NSData*data = [NSDatadataWithContentsOfURL: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 = [selfimageWithURLString:url1];

// 下载第二张图片

NSString*url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";

UIImage *image2 = [selfimageWithURLString: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, ^{

[NSThreadsleepForTimeInterval:5.0];

NSLog(@"第一个项目执行完成。");

});

dispatch_group_async(group, queue2, ^{

[NSThreadsleepForTimeInterval:10.0];

NSLog(@"第二个项目执行完成。");

});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

NSLog(@"集体回调");

});

6.GCD的其他的用法

(1)控制一段代码只执行一次。用在创建单例的时候再好不过了。

//控制代码只执行一次数

for(inti = 1 ;i <= 10 ;i++){

staticdispatch_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);

__blockintcount = 0;

NSLock*lock = [[NSLockalloc]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(@"五秒钟之后执行的代码。");

});

你可能感兴趣的:(iOS多线程的使用)