《Objective-C高级编程 iOS与OS X多线程和内存管理》笔记——GCD部分

以前在iOS里面使用多线程的时候基本都会使用performSelectorInBackground,感觉用着挺好啊,这对简单的程序来说基本就够用了,不需要用到NSThread什么的,也不会想到要去用GCD。o(╯□╰)o

GCD

dispatch_async

dispatch_async的官方文档是这么说的

Submits a block for asynchronous execution on a dispatch queue and returns immediately.

void dispatch_async( dispatch_queue_t queue, dispatch_block_t block);

queue
The queue on which to submit the block. The queue is retained by the system until the block has run to completion. This parameter cannot be NULL.

block
The block to submit to the target dispatch queue. This function performs Block_copy and Block_release on behalf of callers. This parameter cannot be NULL.

This function is the fundamental mechanism for submitting blocks to a dispatch queue. ** Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked. ** The target queue determines whether the block is invoked serially or concurrently with respect to other blocks submitted to that same queue. Independent serial queues are processed concurrently with respect to each other.

意思就是,把需要异步处理的东西放在block中提交到dispatch queue中。其中queue参数就是提交到的那个队列,在block执行完之前,这个queue是被系统retain过的。block即需要异步处理的操作,dispatch_asyn这个方法会自动对block进行Block_copy和Block_release操作。

并且**Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked.**的意思是说dispatch_asyn是立即返回的,并不是等block被调用后才返回的。

在使用GCD的时候,把需要处理的任务放到Block中,然后把任务追加到相应的队列里面,队列叫Dispatch Queue。


Dispatch Queue

Dispatch Quene按照追加的顺序处理任务,在处理任务时存在两种Dispatch Queue,并行(Concurrent dispatch queue)和串行(Serial dispatch queue)。

Serial dispatch queue就是我们认知的那种FIFO的队列。对于该队列中的任务都秉着先进先出的原则。

Concurrent dispatch queue中的任务处理顺序是不一定的。

serial dispatch queue and concurrent dispatch queue

concurrent并行是指使用多个线程来处理多个任务。

假如有7个任务,concurrent dispatch queue执行的示例可以如下:

线程0 线程1 线程2 线程3
task0 task1 task2 task3
task4 task6 task5
task7

假设concurrent dispatch queue准备了4个线程,task0~3分别在四个线程中被执行。线程0在task0执行结束后执行task4。由于线程1中的task1没有结束且线程2中的task2结束了,那么线程2执行task5。

其实下面两张图更能很好的区别serial queue和concurent queue.
serial queue

concurrent queue

block被添加到queue中,将在稍后被执行。具体“稍后”是多少时间,这个很难说清楚,但绝不会很久。

dispatch queue可以自己创建,但是需要自己维护它的生命周期该书第一版是2013年写的,实际从iOS 6.0开始GCD已经纳入了ARC里面,不需要再手工调用dispatch_release了】。

dispatch_queue_t customCocurrentQueue = dispatch_queue_create("com.example.TestGCD", DISPATCH_QUEUE_CONCURRENT);

dispatch_queue_create第一个参数推荐使用应用程序ID逆序,方便调试。第二个参数如果是NULL或者DISPATCH_QUEUE_SERIAL,则表示是serial queue;如果是DISPATCH_QUEUE_CONCURRENT,则表示是concurrent queue。

自己创建的queue需要自己释放

dispatch_release(queue);来释放队列

【从iOS 6.0开始,GCD已经纳入了ARC内,如果要调用dispatch_release,则会报错,如下图】

dispatch_release


Main Dispatch Queue与Global Dispatch Queue

dispatch queue是可以自己创建的,但为了方便,苹果提供了两个队列Main Dispatch Queue和Global Dispatch Queue。

Main Dispatch Queue是在主线程中执行的dispatch queue,因为主线程只有一个,它自然就是serial dispatch queue。一般是将更新UI界面之类的操作放到main dispatch queue中。

Global Dispatch Queue是所有应用程序能够使用的concurrent dispatch queue。Global Dispatch Queue有四个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)、后台优先级(Background Priority)。注意Global dispatch queue的线程并不能保证实时性,优先级只是大致的判断。

使用dispatch_queue_t mainQueue = dispatch_get_main_queue();来获取Main Dispatch Queue。

使用dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);设置优先级,来获取Global Dispatch Queue。

Diaptch Queue的种类如下:

名称 种类 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue(High Priority) Concurrent Dispatch Queue 执行优先级:高
Global Dispatch Queue(Default Priority) Concurrent Dispatch Queue 执行优先级:默认
Global Dispatch Queue(Low Priority) Concurrent Dispatch Queue 执行优先级:低
Global Dispatch Queue(Background Priority) Concurrent Dispatch Queue 执行优先级:后台
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
    NSLog(@"Print 1");
});
dispatch_async(mainQueue, ^{
    NSLog(@"Print 2");
});
dispatch_async(mainQueue, ^{
    NSLog(@"Print 3");
});
dispatch_async(mainQueue, ^{
    NSLog(@"Print 4");
});

NSLog(@"Print 5");

由于上面说过,dispatch_async方法是立即返回的,所以serial queue的打印顺序如下其实并不能通过打印的第五个Log是否在第一个Log之前来断定dispatch_async是立即返回的,因为你无法知道block与dispatch_asyn的调用者所在的句子到底哪个先执行

serial queue

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
dispatch_async(globalQueue, ^{
   NSLog(@"Print 1");
});
dispatch_async(globalQueue, ^{
   NSLog(@"Print 2");
});
dispatch_async(globalQueue, ^{
   NSLog(@"Print 3");
});
dispatch_async(globalQueue, ^{
   NSLog(@"Print 4");
});

NSLog(@"Print 5");

上方的代码concurrent queue的打印顺序如下:

corrent queue

在concurrent queue里面,从Xcode的打印时间来看,是同一时间打印出1~5的。有时候5在第一个,有时候5在第二个,但Xcode上显示的时间是一样的。【其实并不能通过打印的第五个Log是否在第一个Log之前来断定dispatch_async是立即返回的,因为你无法知道block与dispatch_asyn的调用者所在的句子到底哪个先执行


dispatch_once

dispatch_once 可以保证在应用程序中只执行一次,在多线程环境下执行,也可以保证百分百安全。一般用dispatch_once来创建单例。

static dispatch_once_t onceToken;
dispatch_once (&onceToken, ^{

});

注意dispatch_once_t一定要是static类型,如果不是,那么可能在运行时会有奇怪的问题。

关于dispatch_once,有博客对其进行了详细的讲述,待以后来学习。


dispatch_sync

dispatch_asyncdispatch_sync不同之处在于,前者是异步将block加入到queue中,后者是同步将block加入到queue中。所以,前者不等待block执行完成就返回,后者在追加的block结束之前会一直等待。

串行并行形容的是队列,而同步异步形容的是线程。同步线程要阻塞当前线程,等待同步线程中的任务执行完成之后,才能继续执行下一任务;而异步线程则是不用等待。


死锁问题

使用dispatch_sync的时候容易产生死锁问题。例如以下代码:

dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"1");
dispatch_sync(mainQueue, ^{
    NSLog(@"2");
});
NSLog(@"3");

打印的结果如下:

GCD dea

发现“1”之后就没有了!!!进入了死锁!

原因是这样的:

main_queue是serial queue即串行队列,对于其中的任务是使用一个线程来处理的。而队列都是FIFO的,所以后加入的任务是后执行的。最重要的dispatch_sync是同步的,即等到block处理完才会返回。那么就出现了死锁,dispatch_sync在等待2被处理,但是2又是在队列的末尾,需要3处理完之后才能处理2。但是3是dispatch_sync返回之后才会被处理。所以……死锁了o(╯□╰)o

如果换成concurrent queue即并行队列呢?则不会出现死锁。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSLog(@"1");
dispatch_sync(globalQueue, ^{
    NSLog(@"2");
});
NSLog(@"3");
    

gcd deadlock


@synchronized关键字

在使用多线程的时候,为了数据同步,经常会使用@synchronized关键字,通常是@synchronized(self){}来囊括一段代码。而里面的self,表示同一时间只能有一个线程访问该实例的这段代码。

atomic属性

一般在声明一个属性的时候,都会让它的其中一个属性为nonatomic,这是表示是非原子的,而它的反面atomic表示是原子的,即同一时间只能有一个线程访问该属性的getter或者setter。【也就是说,线程A和线程B,A在getter的时候B就不能。PS:但是A在getter的时候B是可以setter的

这就会有一个问题,那就是数据不同步。当一个线程多次读取某个值的时候,取到的值可能不同。

如果要数据同步,读的时候应该是可以有多个线程同时在读的,但写的时候应该是互斥的,且写与读不能同时进行。为了解决数据不同步的问题,可以使用dispatch barrier来实现。

dispatch_barrier_async

一个dispatch_barrier_async允许在一个并行队列中创建一个同步点。当遇到barrier,会延迟barrier所在的block,把barrier之前的block都执行完之后再开始执行barrier。所以在barrier之后提交的block,会在barrier执行完成之后才会被执行。

所以就读写数据不同步的问题,可以在并行队列中进行读操作,当要写的时候就设置barrier,当写完成之后,再提交其他的读操作的block。

dispatch_after

dispatch_after可以让一个block延迟提交到队列中,而不是延迟执行。虽说有时候可以来代替NSTimer,但使用dispatch_after提交的block被执行的时间是不确定的,如果在它之前,队列中有其他耗时操作的话,会受影响。

这篇博客里对比了NSTimer、performSelector和NSTimer,具体的区别在于:

1.NSTimer和performSelector都需要有一个活跃的runloop。主线程是默认开启runloop的,所以可以在主线程中直接调用NSTimer的scheduleTimerxxx 和NSObject的performSelector。但如果是在子线程,那么需要手动激活runloop。

2.NSTimer、performSelector是可以取消的,但是dispatch_after就不能取消了(在2016-10-17新增的部分,已有更改)。NSTimer或perfromSelector的创建和取消必须在同一个线程中。

3.使用NSTimer的时候要注意内存的管理。使用perfromSelectorInBackground的时候,系统会自动创建NSThread,如果多次调用该方法,那么就会创建多个NSThread。


2016-10-17 新增

取消dispatch_after的任务

如果想要取消dispatch_after提交的任务,那么有两种方法:

1.使用一个标志位,如果为NO的时候,就不执行。

2.使用iOS 8新增的dispatch_block_cancel方法。官方文档里面说这个方法的参数一定要是使用dispatch_block_create方法创建的block。并且对已经在执行的block不能取消。

具体使用方法如下:

    dispatch_block_t b2 = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^(){
        NSLog(@"b2 block");
    });
    dispatch_time_t time2 = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
    dispatch_after(time2, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), b2);
    dispatch_block_cancel(b2);

dispatch_notify


参考

  1. https://www.bignerdranch.com/blog/dispatch_once-upon-a-time/

  2. http://www.dreamingwish.com/article/gcd-guide-dispatch-once-1.html

  3. 官方文档

Show Comments