异步实现的记录总结

GCD

Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般讲应用程序中记述的线程管理用的代码在系统级中实现,也就是基于iOS的UNX内核。多线程编程会导致很多问题,比如 资源竞争、死锁和太多线程导致消耗大量内存 等。

开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中

Dispatch Queue

介绍

“Dispatch Queue”就是执行处理的等待队列,他会按照追加的顺序(先进先出 FIFO)执行处理。

两种Dispatch Queue

  • Serial Dispatch Queue 等待现在执行中处理结束。但是,如果生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue将并行执行。
  • Concurrent Dispatch Queue 不等待现在执行中处理结束。可以并行执行多个处理,但执行的数量取决于当前系统的状态,即iOS和OS X基于Dispatch Queue中的处理数、CPU核数、CPU负载等当前的系统状态来决定并行执行的处理数。

根据是否有数据竞争来判定使用哪种类型。

得到Dispatch Queue
第一种方法

使用dispatch_queue_create来得到,自己的。

  • 第一个参数指定Dispatch Queue的名称,推荐使用逆序全程域名。可以设置为NULL,但是最好不要,该名称会在Xcode和Instrument的调试器中作为Dispatch Queue名称出现,也会出现在应用程序崩溃时所产生的CrashLog中。

  • 第二个参数执行生成Dispatch Queue的类型。生成Serial Dispatch Queue时,一般设置为NULL就行了,也可以设置为DISPATCH_QUEUE_SERIAL。生成Concurrent Dispatch Queue时,设置为DISPATCH_QUEUE_CONCURRENT

    dispatch_queue_t queueSerial = dispatch_queue_create("com.jiayoufang.gcdbenchmark.serialqueue", NULL);
    dispatch_queue_t queueConcurrent = dispatch_queue_create("com.jiayoufang.gcdbenchmark.concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
    

关于内存:会自动处理
在dispatch_async函数中追加Block到Dispatch Queue后,Block会通过dispatch_retain 函数持有Dispatch Queue,所以即使Dispatch Queue被立即释放,他也不会被销毁。

第二种方法

获取系统标准提供的Dispatch Queue

  • Main Dispatch Queue 是在主线程中执行的Dispatch Queue,因为主线程只有一个,所以Main Dispatch Queue自然就是Serial Dispatch Queue。追加到它上面的处理在主线程的Runloop中执行,可以处理用户的界面更新等操作。

    dispatch_queue_t queueMain = dispatch_get_main_queue();
    
  • Global Dispatch Queue 是所有应用程序都可使用的Concurrent Dispatch Queue。他有4个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。通过XNU内核管理的Global Dispatch Queue的线程,将各自使用Global Dispatch Queue的执行优先级作为线程的执行优先级,但并不能保证线程的实时性,因此执行优先级只是大致判断。

    //获取默认优先级的Global Dispatch Queue
    dispatch_queue_t queueGlobal = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

变更生成的Dispatch Queue的执行优先级

dispatch_queue_create生成的 Dispatch Queue,不管是Serial Dispatch Queue还是 Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。变更优先级使用dispatch_set_target_queue函数。
例如

dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.jiayoufang.queue", NULL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(serialDispatchQueue, globalQueue);
  • 第一个参数指定要变更优先级的Dispatch Queue。不可指定系统提供的 Main Dispatch Queue 和 Global Dispatch Queue。
  • 第二个参数指定与要使用的执行优先级相同优先级的Global Dispatch Queue。

指定时间后执行处理

使用dispatch_after来实现。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
    
});

需要注意的是:dispatch_after并不是在指定时间后执行处理,而是在指定时间追加处理到Dispatch Queue。

dispatch_after的底层其实是用dispatch_source实现的,不依赖runloop。

dispatch_time_t的创建
  • 指定相对时间

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC); 
    

“ull”是C语言的数值字面量,是显式表明类型时使用的字符创(表示“unsigned long long)。

  • 指定绝对时间

    dispatch_time_t time1 = getDispatchTimeByDate([NSDate dateWithTimeIntervalSinceNow:3]);
    
    dispatch_time_t getDispatchTimeByDate(NSDate *date){
        NSTimeInterval timeInterval;
        double second,subsecond;
        struct timespec time;
        dispatch_time_t timestone;
    
        timeInterval = [date timeIntervalSince1970];
        subsecond = modf(timeInterval, &second);
        time.tv_sec = second;
        time.tv_nsec = subsecond * NSEC_PER_SEC;
        timestone = dispatch_walltime(&time, 0);
        return timestone;
    }
    

Dispatch Group

在追加到Dispatch Queue的多个处理全部结束后想执行结束处理。

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    NSLog(@"Block0");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"Block1");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"Block2");
});

dispatch_group_notify(group, queue, ^{
    NSLog(@"Done");
});

如果仅仅只是验证是否结束,也可以使用dispatch_group_wait

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

第二个参数表示等待的时间(超时时间)。他属于dispatch_time_t的值。dispatch_group_wait是有返回值的,如果返回值不为0,说明虽然经过了指定的时间,但是属于Dispatch Queue的某一个处理还在进行中。如果返回值为0,说明全部处理执行结束。
时间指定为DISPATCH_TIME_FOREVER,返回值肯定为0。
指定为DISPATCH_TIME_NOW,不用等待,直接判定属于Dispatch Group的处理是否执行结束。

需要注意的是,dispatch_group_wait,他会阻塞当前线程,如有必要,可以使用dispatch_async将整个方法放入后台队列以避免线程阻塞。

或者使用dispatch_group_notify来观察Dispatch Group中的任务是否执行完。

多个异步请求判断都结束了

参考了叶大神的在这里

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_main_queue(), ^{
        int i = 0;
        while (i < 1000) {
            NSLog(@"Group1 : %d",i++);
        }
        dispatch_group_leave(group);
    });
    
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_main_queue(), ^{
        int i = 0;
        while (i < 1000) {
            NSLog(@"AAAAAA  Group : %d",i++);
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"Done");
    });

dispatch_barrier_async

可以达到 等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕之后,Concurrent Dispatch Queue再恢复一般的动作。

dispatch_queue_t queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"A");
});
dispatch_async(queue, ^{
    NSLog(@"B");
});
dispatch_async(queue, ^{
    NSLog(@"C");
});
dispatch_barrier_async(queue, ^{
    NSLog(@"A、B、C 先执行完");
});
dispatch_async(queue, ^{
    NSLog(@"D");
});
dispatch_async(queue, ^{
    NSLog(@"E");
});
dispatch_barrier_async(queue, ^{
    NSLog(@"D、E 先执行完");
});
dispatch_async(queue, ^{
    NSLog(@"F");
});
dispatch_async(queue, ^{
    NSLog(@"G");
});

dispatch_apply

该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并会等待全部处理执行结束

dispatch_queue_t queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"index : %zd",index);
});

例如对NSArray类对象的所有元素执行处理,不必一个一个编写for循环部分。

Note:由于dispatch_apply会和dispatch_sync函数相同,会等待处理结束。因此,推荐在dispatch_async函数中非同步的执行dispatch_apply函数。

线程挂起和恢复

挂起指定的Dispatch Queue

dispatch_suspend(queue);

恢复指定的Dispatch Queue

dispatch_resume(queue);

这些函数对已经执行过的处理没有影响。挂起后,尚未执行的处理会停止执行,恢复之后这些处理能够继续执行。

dispatch_semaphore_t

创建一个信号量。参数指定信号量的起始值,这个数字是可以访问的信号量。需要注意的是,如果初始化为0,那么在使用信号量时必然会被阻塞,也是使用这个方式来解决测试用例时的异步代码问题。

加锁的实现

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t sempaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc]init];
for (NSInteger i = 0 ; i < 10000 ; i++) {
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sempaphore, DISPATCH_TIME_FOREVER);
        
        [array addObject:[NSNumber numberWithInteger:i]];
        
        dispatch_semaphore_signal(sempaphore);
    });
}

在使用Xcode中的 Product/Test 运行测试时,测试是在主线程运行的,所以可以假设所有的测试都是串行发生的。在一个给定的测试方法运行完成,XCTest方法将考虑此次测试已结束,并进入下一个测试,这就意味着任何来自目前一个测试的异步代码会在下一个测试运行时继续发生。解决这个问题:

- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    dispatch_queue_t queue = dispatch_queue_create("com.ivan.queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"执行了测试");
    dispatch_async(queue, ^{
        for (NSUInteger i = 0; i < 888; i++) {
            NSLog(@"********* : %zd",i);
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, 1);
    if(dispatch_semaphore_wait(semaphore, timeoutTime)){
        XCTFail("Time out");
    }
}

Dispatch Source

GCD 除了主要的Dispatch Queue外,还有Dispatch Source。
实现一个简单的定时器,说明用法

    - (void)test7{
    //实现一个定时器的例子
    __block NSInteger count = 0;
    //指定 DISPATCH_SOURCE_TYPE_TIMER,作成 Dispatch Source。在定时器经过指定时间时设定Main Dispatch 为追歼处理的Dispatch Queue。
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    //将定时器设置为2秒后,允许延迟1秒
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);
    //指定定时器指定时间内执行的处理
    dispatch_source_set_event_handler(timer, ^{
        
        NSLog(@"执行");
        if (count >= 4) {
            NSLog(@"Count >= 4");
            //执行完,之后,取消Dispatch Source
            dispatch_source_cancel(timer);
        }else{
            NSLog(@"Count < 4");
            count++;
        }
    });
    
    //指定取消Dispatch Source时的处理
    dispatch_source_set_cancel_handler(timer, ^{
        NSLog(@"cancel");
        //释放自身
     //        dispatch_release(timer);
    });
    
    //启动Dispatch Source
    dispatch_resume(timer);
    }

另外,dispatch_source_t的本质其实是一个OC对象,不要被骗了,可以打印验证(这是个方法,要记住

//打印出来的内容是类名+内存地址
    NSLog(@"%@",timer);

如果声明全局变量,需要强引用

@property(nonatomic,strong) dispatch_source_t timer;

NSOperation

NSOperation是基于GCD实现的,由于是面向对象的,可能看着会更舒服一些。

实现步骤

  • 将要执行的操作封装到一个NSOperation对象中
  • NSOperation对象添加到NSOperationQueue
  • 系统会自动将NSOperationQueue中的NSOperation取出来

NSOperation说明

NSOperation是一个抽象类,并不具备封装操作的能力,必须使用它的子类

  • NSInvocationOperation
  • NSBlockOperation
  • 自定义子类继承NSOperation来实现内部响应的方法

Note:默认情况下,调用了start方法之后并不会开一条新线程去执行操作,而是在当前线程同步执行操作。只有当NSOperation添加到一个NSOperationQueue中,才会异步执行操作。但NSBlockOperation只要封装数大于1,就会异步执行操作。

使用NSInvocationOperation

- (void)testInvocationOperation
{
    NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operation) object:nil];
    [op start];
}

- (void)operation{
    NSLog(@"---%@",[NSThread currentThread]);
}

最后的打印结果是: ---<NSThread: 0x7fb5e9707b20>{number = 1, name = main}
说明这样操作的话他并没有开启新的线程。

使用NSBlockOperation

如果只是添加一个block的话,也是不会开启新的线程来进行操作的

- (void)testBlockOperation
{
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1---%@",[NSThread currentThread]);
    }];
    
    [op start];
}

但是如果添加多个block则会开启新的线程,但不是一定开启

- (void)testBlockOperation
{
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1---%@",[NSThread currentThread]);
    }];
    
    
    [op addExecutionBlock:^{
        NSLog(@"2---%@",[NSThread currentThread]);
    }];
    
    [op addExecutionBlock:^{
        NSLog(@"3---%@",[NSThread currentThread]);
    }];
    [op start];
}

NSOperationQueue

类型:

  • 主队列
  • [NSOperationQueue mainQueue]
  • 凡是添加到主队列中的任务(NSOperation),都会放到主线程中执行。
  • 非主队列
  • [[NSOperationQueu alloc]init]
  • 同时包含了:串行、并发功能
  • 添加到这种队列中的任务,就会自动放到子线程中执行。

使用suspend属性来暂停,已经启动的线程是无法停止的,只能停止接下来要执行的操作。
但是使用cancelAllOperaion也不会停止,所以在自定义NSOperation的时候,我们可以采取一种方法来增强体验,就是没执行完一个长时间的操作,就判定一下是否取消。

#import "CustomOperation.h"

@implementation CustomOperation

- (void)main
{
    for (NSInteger i = 0; i < 1000; i++) {
        NSLog(@"操作1 %zd",i);
    }
    
    //就是这样
    if (self.isCancelled) {
        return;
    }
    
    for (NSInteger i = 0; i < 1000; i++) {
        NSLog(@"操作2 %zd",i);
    }
    
    if (self.isCancelled) {
        return;
    }
    
    for (NSInteger i = 0; i < 1000; i++) {
        NSLog(@"操作3 %zd",i);
    }
}

@end
依赖和监听

依赖就是执行其中一个之后才可以执行。
Note : 实现很简单,主要是要理解是什么意思,还有就是千万不要循环了。但是可以在不同队列之间设置依赖,还是比较牛逼的

监听需要注意的是不一定是在同一条线程中执行,但是是在子线程中的

- (void)testDependency
{
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation1");
    }];
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation2");
    }];
    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation3");
    }];
    
    operation3.completionBlock = ^(){
        NSLog(@"监听执行完成");
    };
    [operation3 addDependency:operation1];
    [operation2 addDependency:operation3];
    
    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];
}

推荐阅读更多精彩内容