Cocoa Touch(四): 多线程GCD, NSObject, NSThread, NSOperationQueue

    多线程的重要性没必要多言,现代操做系统不可能离开进程线程的抽象。具体到ios应用,咱们只能在一个进程中管理线程,主线程不该该去执行很是耗时间的后台操做致使出现卡机现象,后台的事情交给后台线程来完成。ios

 

Grand Central Dispatch程序员

GCD组件包含两大部分,第一个部分是提供了C语言线程同步互斥接口,第二个部分就是dispatch队列。web

dispatch block的执行最终都会放进某个队列中去进行,它相似NSOperationQueue但更复杂也更强大,而且能够嵌套使用。因此说,结合block实现的GCD,把函数闭包(Closure)的特性发挥得淋漓尽致。数据库

 

dispatch队列的生成能够有这几种方式:编程

1. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.serial", DISPATCH_QUEUE_SERIAL); //生成一个串行队列,队列中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。第一个参数是队列的名称,在调试程序时会很是有用,全部尽可能不要重名了。安全

2. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent", DISPATCH_QUEUE_CONCURRENT); //生成一个并发执行队列,block被分发到多个线程去执行网络

3. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //得到程序进程缺省产生的并发队列,可设定优先级来选择高、中、低三个优先级队列。因为是系统默认生成的,因此没法调用dispatch_resume()和dispatch_suspend()来控制执行继续或中断。须要注意的是,三个队列不表明三个线程,可能会有更多的线程。并发队列能够根据实际状况来自动产生合理的线程数,也可理解为dispatch队列实现了一个线程池的管理,对于程序逻辑是透明的。多线程

官网文档解释说共有三个并发队列,但实际还有一个更低优先级的队列,设置优先级为DISPATCH_QUEUE_PRIORITY_BACKGROUND。Xcode调试时能够观察到正在使用的各个dispatch队列。闭包

4. dispatch_queue_t queue = dispatch_get_main_queue(); //得到主线程的dispatch队列,注意这个队列中的任务须要被主线程完成。一样没法控制主线程dispatch队列的执行继续或中断。并发

接下来咱们可使用dispatch_async或dispatch_sync函数来加载须要运行的block。

dispatch_async(queue, ^{

  //block具体代码

}); //异步执行block,函数当即返回

dispatch_sync(queue, ^{

  //block具体代码

}); //同步执行block,函数不返回,一直等到block执行完毕。编译器会根据实际状况优化代码,因此有时候你会发现block其实还在当前线程上执行,并没用产生新线程。

实际编程经验告诉咱们,尽量避免使用dispatch_sync,嵌套使用时还容易引发程序死锁。

若是queue1是一个串行队列的话,这段代码当即产生死锁:

dispatch_sync(queue1, ^{

dispatch_sync(queue1, ^{

    ......

  });

  ......

 });

在主线程当中,为何下面代码也确定死锁:

dispatch_sync(dispatch_get_main_queue(), ^{

  ......

});

 

那实际运用中,通常能够用dispatch这样来写,常见的网络请求数据多线程执行模型:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

  //子线程中开始网络请求数据

  //更新数据模型

  dispatch_sync(dispatch_get_main_queue(), ^{

    //在主线程中更新UI代码

  });

});

程序的后台运行和UI更新代码紧凑,代码逻辑一目了然。

 

dispatch队列是线程安全的,能够利用串行队列实现锁的功能。好比多线程写同一数据库,须要保持写入的顺序和每次写入的完整性,简单地利用串行队列便可实现:

dispatch_queue_t queue1 = dispatch_queue_create("com.dispatch.writedb", DISPATCH_QUEUE_SERIAL);

- (void)writeDB:(NSData *)data

{

  dispatch_async(queue1, ^{

    //write database

  });

}

下一次调用writeDB:必须等到上次调用完成后才能进行,保证writeDB:方法是线程安全的。

 

dispatch队列还实现其它一些经常使用函数,包括:

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t)); //重复执行block,须要注意的是这个方法是同步返回,也就是说等到全部block执行完毕才返回,如需异步返回则嵌套在dispatch_async中来使用。多个block的运行是否并发或串行执行也依赖queue的是否并发或串行。

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); //这个函数能够设置同步执行的block,它会等到在它加入队列以前的block执行完毕后,才开始执行。在它以后加入队列的block,则等到这个block执行完毕后才开始执行。

void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block); //同上,除了它是同步返回函数

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block); //延迟执行block

最后再来看看dispatch队列的一个颇有特点的函数:

void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

它会把须要执行的任务对象指定到不一样的队列中去处理,这个任务对象能够是dispatch队列,也能够是dispatch源。并且这个过程能够是动态的,能够实现队列的动态调度管理等等。好比说有两个队列dispatchA和dispatchB,这时把dispatchA指派到dispatchB:

dispatch_set_target_queue(dispatchA, dispatchB);

那么dispatchA上还未运行的block会在dispatchB上运行。这时若是暂停dispatchA运行:

dispatch_suspend(dispatchA);

则只会暂停dispatchA上原来的block的执行,dispatchB的block则不受影响。而若是暂停dispatchB的运行,则会暂停dispatchA的运行。

这里只简单举个例子,说明dispatch队列运行的灵活性,在实际应用中你会逐步发掘出它的潜力。

 

优势:简单方便,直接捕获变量进入block(只读访问,除非用__block声明)。

缺点:dispatch队列暂时不支持cancel(取消),没有实现dispatch_cancel()函数,不像NSOperationQueue。

 

ps: GCD所包含的方法并不限于任务队列,还提供了 dispatch_semaphore_signal/dispatch_semaphore_wait 这样的C语言接口(全部C语言代码都能用OC编译器编译),并不会强制要求程序员必定要面向对象,避免了不少繁琐步骤。

ps: !!! 重要的一点:block并非被主线程所执行,若是其它的线程在执行这个block是,调用了相似 _webView = [[UIWebView alloc] init]这样的语句,那么就会Crash:

Tried to obtain the web lock from a thread other than the main thread or the web thread. 
This may be a result of calling to UIKit from a secondary thread. Crashing now...

解决办法就是,经过performSelectorOnMainThread或者GCD dispatch_的方式强制让某些语句被主线程执行,或者经过信号量实现同步的方式来发送网络请求,从而保证由主线程来调用全部涉及UI的语句。 

 

NSObject

全部继承于NSObject类的类实例,均可以调用启动多线程的方法。

如下3个函数是多线程的方法: 

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg,建立一个真正的新线程,不过没法引用到这个线程

[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES]; 通知主线程执行操做,通常用于更新界面

[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];  等待selector指定的函数被某个线程执行完成后,当前线程再继续当前任务

如下2个函数实际上不是多线程:

[self performSelector:@selector(run) withObject:nil];等待selector指定的函数被当前线程执行完成后,当前线程再继续当前任务

performSelector:withObject:afterDelay:当前线程执行完成后,再启动线程去执行selector所选择的方法

 

Objective-C中调用函数的方法是“消息传递”,这个和普通的函数调用的区别是,你能够随时对一个对象传递任何消息,而不须要在编译的时候声明这些方法。因此Objective-C能够在runtime的时候传递人和消息。

 

优势:简单方便,而且容许在运行时动态调用一个对象的任意方法

缺点:若是须要传递参数,不如GCD

 

NSThread

NSThread类是轻量级的多线程开发的类,使用起来也并不复杂,可是使用NSThread须要本身管理线程生命周期。NSThread中封装了方法:
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument 建立一个真实的新线程,不过没法引用到这个线程

- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 建立一个线程对象,能够引用到一个新线程,调用start方法建立真实的线程。

[thread start]; 建立一个真实的线程,而且把线程对象thread做为这个线程的引用

[thread cancel]; 取消对象thread所引用的那个线程

这个类命名空间下还提供了一些实用的类方法:

NSThread *current = [NSThread currentThread]; 获取当前线程

NSThread *main = [NSThread mainThread]; 获取主线程

[NSThread sleepForTimeInterval:2];  或  [NSThread sleepUntilDate:date];  暂停正在执行这段代码的进程,也就是当前进程

 

优势:以面对对象的观点引用线程,方便线程生命周期的管理和多线程之间的同步;这个类能够配合performselector方法使用;提供了一些实用的类方法,如获取当前线程的引用、暂停当前线程等。

缺点:须要定义单独的线程类来表明新的线程,较为繁琐,不够直接

 

 

 NSOperation

 咱们先直接经过代码来领会一个任务队列。

invocation pperation(经过selector指定任务)

-(void)loadImageWithMultiThread{
    /*建立一个调用操做
     object:调用方法参数
    */
    NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //进程经过start方法后才能启动操做,可是注意若是直接调用start方法,则此操做会在主线程中调用,通常不会这么操做,而是添加到NSOperationQueue中
//    [invocationOperation start];
    
    //建立操做队列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    //注意添加到操做队后,队列会开启一个线程执行此操做
    [operationQueue addOperation:invocationOperation];
}

block operation(经过block指定任务)

NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数
[operationQueue addOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
//更新UI界面,此处调用了主线程队列的方法(mainQueue是UI主线程)
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self updateImageWithData:data andIndex:i];
}];

概念上,一个任务队列operation queue至关于一个由不少线程和任务组成的池。在一些应用场景之下,好比当须要下载不少张图片,能够把下载任务和相应的线程统一交给operation queue来管理,方便了操做。

一个NSOperation的实例定义了一个任务,能够经过调用start方法在当前线程启动它对应的任务,不过通常不这么作,而是把operation加入Operation queue中统一管理。对于invocation operation和block operation,两种方式本质没有区别,可是是后者使用Block形式进行代码组织,传参容易,使用相对方便。

 

优势:统一管理大量的任务和线程,适用于下载大量图片等场景。

 

 

多线程的应用

一、操做数据模型

多个任务同时操做数据模型,线程安全须要仔细思考,对于数据模型对象的操做须要用@synchronized。

二、网络请求

网络请求是多线程的重要应用,下一篇博客会讲。

相关文章
相关标签/搜索