Objective-C之GCD多线程(二)

前言

Objective-C之GCD多线程(一)中,我们了解了一些常见常用的GCD的API。本文在前文的基础上,再介绍一下几个API:

  • dispatch_set_target_queue
  • dispatch_after
  • Dispatch Group
  • dispatch_barrier_async
  • dispatch_sync
  • dispatch_apply
  • dispatch_suspend/dispatch_resume
  • dispatch_Semaphore
  • dispatch_once
  • Dispatch I/O

dispatch_set_target_queue

dispatch_queue_create函数生成的两种Dispatch Queue的优先级都与默认优先级Global Dispatch Queue相同。当我们需要改变由dispatch_queue_create函数生成的Dispatch Queue的优先级就需要用到dispatch_set_target_queue函数了。例如,我们更改一个Serial Queue的优先级为低优先级:

// 创建了默认优先级的Serial Queue
    dispatch_queue_t serialQueue = dispatch_queue_create("com.larry.GcdTest", NULL);
    // 获取一个低优先级Concurrent Queue
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    // 将Serial Queue优先级变为低
    dispatch_set_target_queue(serialQueue, globalQueue);

dispatch_set_target函数有2个参数:

  • 第一个为需要转换的队列
  • 第二个为目标队列,需要转换的队列在转换后优先级同目标队列

利用dispatch_queue_create函数我们还可以将多个并行执行的Serial Queue转换为串行执行,被转换的队列会被添加到目标队列中串行执行。如下图所示

并行执行的Serial Queue
利用dispatch_queue_create函数转换成串行执行

下面我们演示一下:

并行执行的Serial Queue

利用dispatch_set_target_queue函数转换为串行队列:

转换后变为串行队列

dispatch_after

dispatch_afterAPI主要用于想要延迟执行处理。例如:想在10秒后执行响铃,就可以使用dispatch_after来实现。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    __block NSDate *sendDate=[NSDate date]; // 获得当前时间
    NSDateFormatter *dateformatter=[[NSDateFormatter alloc] init]; // 设置时间格式
    [dateformatter setDateFormat:@"HH:mm"];
    NSString *timeBegain = [dateformatter stringFromDate:sendDate];
    NSLog(@"开始时间:%@",timeBegain);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        sendDate = [NSDate date];
        NSString *timeEnd = [dateformatter stringFromDate:sendDate];
        NSLog(@"闹钟响时间:%@",timeEnd);
    });
    while (1) {
        // 防止程序结束
    }
    return 0;
}

运行结果:

运行结果

可能心细的朋友会发现,不是延迟10秒执行吗?但是为什么图片中不是10秒而是11秒?因为dispatch_after是在指定时间之后将处理添加到queue中,但是在什么时候执行需要看queue中的情况!
dispatch_after函数有2个参数,第一个参数是由dispatch_time函数生成的dispatch_time_t类型的参数。dispatch_after函数能从dispatch_time函数的第一个参数中获取开始时间,从第二个参数中获取延迟执行的时间;第二个参数是指定要追加操作的队列;第三个参数则是需要执行的Block。dispatch_time函数中的第二个参数中的NSEC_PER_SEC代表秒,还有一个NSEC_PER_MSEC则代表毫秒。

Dispatch Group

当追加到Dispatch Queue中的多个处理全部执行结束之后,我们通常都会需要执行结束处理。当使用Serial Queue的时候很简单,只需要将全部处理添加到一个Serial Queue中,结束处理在最后添加就可以实现。但是使用Concurrent Queue的时候,就很复杂。所以我们需要利用Dispatch Group。
例如,追加3个处理到Global Queue(Concurrent Queue)中,在3个处理结束后执行结束处理,我们可以这样做:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    // 创建一个Serial Queue,用于执行完成处理
    dispatch_queue_t serialQueue = dispatch_queue_create("com.Larry.GCDTEST", NULL);
    // 创建默认优先级Concurrent Queue
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    // 创建Dispatch Group
    dispatch_group_t group = dispatch_group_create();
    // 添加操作
    dispatch_group_async(group, concurrentQueue, ^{printf("处理1\n");});
    dispatch_group_async(group, concurrentQueue, ^{printf("处理2\n");});
    dispatch_group_async(group, concurrentQueue, ^{printf("处理3\n");});
    // 操作完成执行
    dispatch_group_notify(group, serialQueue, ^{printf("处理全部完成\n");});
    while (1) {
    }
    return 0;
}
执行结果

除了dispatch_group_notify函数可以判断执行完成外,我们还可以利用dispatch_group_wait函数来进行判断。

dispatch_group_wait

我们可以将以上代码转换为以下形式:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    // 创建一个Serial Queue,用于执行完成处理
    dispatch_queue_t serialQueue = dispatch_queue_create("com.Larry.GCDTEST", NULL);
    // 创建默认优先级Concurrent Queue
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
    // 创建Dispatch Group
    dispatch_group_t group = dispatch_group_create();
    // 添加操作
    dispatch_group_async(group, concurrentQueue, ^{printf("处理1\n");});
    dispatch_group_async(group, concurrentQueue, ^{printf("处理2\n");});
    dispatch_group_async(group, concurrentQueue, ^{printf("处理3\n");});
    // 操作完成执行
    long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    if (result == 0) {
        dispatch_async(serialQueue, ^{printf("处理结束");});
    }
    else{
        // 处理没结束的程序段
    }
    while (1) {
        // 防止程序结束
    }
    return 0;
}

运行结果:

使用dispatch_group_wait函数结果

关于dispatch_group_wait函数,有两个参数和一个返回值。第一个参数是dispatch_group_t类型,我们需要传递需要进行完成处理的Group;第二个参数为dispatch_time_t类型的变量。这个函数的返回值为0代表Group中的执行结束,不为0则代表指定时间到了,但是Group中的执行还没有完成。所以我们只需要对返回值进行判断,就能知道Group中的处理是否完成。上面的例子中,我们将时间设置为永久等待,所以只有Group中的处理全部完成才会得到返回值,所以返回值的值恒为0。
我们可以根据需要来设置由时间是否到达还是处理是否全部完成来执行收尾工作。相比较dispatch_group_notify而言,dispatch_group_wait函数更灵活一些。

dispatch_barrier_async

我们在使用多线程的时候,在对数据进行处理的时候,很大的可能会碰到数据竞争的问题。如前文所述,我们可以使用Serial Dispatch Queue来避免这个问题。但是当对数据的读取操作是分开的时候,也就是读取处理和读取处理并行执行,那么使用Concurrent Dispatch Queue也是不会有问题的。当我们在多个读取中遇到了一个写入操作时,该怎么办?这个时候我们就可以使用dispatch_barrier_async来解决这个问题。
在使用dispatch_barrier_async函数的时候,它会等待当前Concurrent Dispatch Queue中的处理执行结束后,再将该处理追加到Concurrent Dispatch Queue,也就是
dispatch_barrier_async函数所携带的处理单独占用Concurrent Dispatch Queue。在执行dispatch_barrier_async函数的时候,它会屏蔽外界追加到Concurrent Dispatch Queue的操作。当它的处理执行完成后,Concurrent Dispatch Queue才开始继续正常添加操作。如下图所示:

过程演示

dispatch_sync

async意味着非同步,意思就是处理各自管各自的执行;而与之相反sync意味着同步,需要一个接一个的执行。我们直接演示:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    // 创建一个Concurrent dispatch Queue
    dispatch_queue_t concurrentdispatchQueue = dispatch_queue_create("com.larry.gcd.serialTest", DISPATCH_QUEUE_CONCURRENT);
    // Serial dispatch Queue
    dispatch_sync(concurrentdispatchQueue, ^{NSLog(@"1");});
    dispatch_sync(concurrentdispatchQueue, ^{NSLog(@"2");});
    dispatch_sync(concurrentdispatchQueue, ^{NSLog(@"3");});
    dispatch_sync(concurrentdispatchQueue, ^{NSLog(@"4");});
    dispatch_sync(concurrentdispatchQueue, ^{NSLog(@"5");});
    dispatch_sync(concurrentdispatchQueue, ^{NSLog(@"6");});
    // 防止程序结束
    while (1) {
    }
    return 0;
}

Concurrent Dispatch Queue是并行执行的,使用async的时候是乱序的,但是使用了sync的时候就是一个接一个执行:

使用sync的Concurrent Dispatch Queue

dispatch_apply

dispatch_apply函数是和Dispatch Group关联的API,它的作用是按指定的次数将制定的Block追到制定的Dispatch Queue中,并等待全部处理执行结束后才开始继续往下走
该函数有三个参数,第一个为重复次数,第二个参数为追加对象的Dispatch Queue,第三个参数的Block为带有参数的Blcok,其参数相当于for循环中的i的作用。所以我们可以用dispatch_apply来实现for循环的功能。

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    // 创建一个Concurrent dispatch Queue
    dispatch_queue_t concurrentdispatchQueue = dispatch_queue_create("com.larry.gcd.serialTest", DISPATCH_QUEUE_CONCURRENT);
    dispatch_apply(10, concurrentdispatchQueue, ^(size_t index) {
        NSLog(@"%zu",index);
    });
    NSLog(@"完成");
    // 防止程序结束
//    while (1) {
//    }
    return 0;
}

执行结果:

dispatch_apply执行结果

dispatch_suspend/dispatch_resume

这两个函数前者用来挂起队列,队列挂起后,追加到队列中但还没有开始执行的处理,将暂停执行;后者用来恢复队列,恢复队列后,暂停的处理继续执行。使用方式如下:

dispatch_suspend(Queue)// 挂起队列
dispatch_resume(Queue)// 暂停队列

在前文提到的dispatch_set_target_queue函数中,目标队列如果被挂起,那么被转换的队列也会相同的挂起。但是被转换的队列被挂起,目标队列则不受影响!

Dispatch Semaphore

Dispatch Semaphore和操作系统原理中的信号量一样,都是用来避免数据竞争这一类问题的。前文讲到过dispatch_barrier_async这个函数,但是此函数对数据竞争处理的对象的粒度更细。
Dispatch Semaphore是持有计数的信号,当它的计数大于1时,就会对其进行减1并执行处理;当计数为0或小于0的时候,就会等待下去。更加详细的请自行参阅:《操作系统原理》。
通常使用Dispatch Semaphore的时候会使用以下函数:

函数 作用
dispatch_semaphore_create 创建信号量
dispatch_semaphore_wait 此函数对信号量进行控制,当信号量值大于等于1的时候减1,然后返回;当小于等于0的时候,则在设置的时间内等待
dispatch_semaphore_signal 将信号量加1

下面举例使用:

##import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    // 创建一个Concurrent dispatch Queue
    dispatch_queue_t concurrentdispatchQueue = dispatch_queue_create("com.larry.gcd.serialTest", DISPATCH_QUEUE_CONCURRENT);
    // 提供一个信号量,也就意味着同时只有1个线程能对资源进行操作
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for (int i =0; i<1000; ++i) {
        dispatch_async(concurrentdispatchQueue, ^{
            // 对信号量资源进行判断,当信号量大于等于1的时候,将信号量减一(相当于消耗一个资源),并返回,设置时间为永远等待
            // 如果此函数没有返回,则阻塞在这里,等待资源。
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [array addObject:[NSNumber numberWithInt:i]];
            // 处理完成,信号量加一(相当于释放一个资源)
           dispatch_semaphore_signal(semaphore);
        });
    }
    return 0;
}

我们将for循环执行1000次,如果没有使用Dispatch Semaphore来限制资源,那么同时对arry访问的的操作就很多,会导致程序异常结束,但是用Dispatch Semaphore对资源进行限制的时候,程序就能稳定的进行下去。

dispatch_once

dispatch_once函数是保证在应用程序执行中只执行一次指定处理的API。通常在建立单例中使用。

static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 初始化单例
    });

Dispatch I/O

在读取较大文件的时候将文件切割开来,Dispatch I/O使用Global Dispatch Queue来同步读取,加快读取速度。利用dispatch_io_set_low_water来设置一次读取的大小,dispatch_io_read函数使用Global Dispatch Queue来并列读取,当读取结束后会回调Block中的代码进行相关资源合并。
请看下面苹果中使用Dispatch I/O 和 Dispatch Data的例子。下面的代码摘自Apple System Log API里的源代码

// 创建串行队列  
    pipe_q = dispatch_queue_create("PipeQ", NULL);  
    // 创建 Dispatch I/O  
    pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){  
        close(fd);  
    });  

    *out_fd = fdpair[1];  

    // 该函数设定一次读取的大小(分割大小)  
    dispatch_io_set_low_water(pipe_channel, SIZE_MAX);  
    //  
    dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){  
        if (err == 0) // err等于0 说明读取无误  
        {  
            // 读取完“单个文件块”的大小  
            size_t len = dispatch_data_get_size(pipedata);  
            if (len > 0)  
            {  
                // 定义一个字节数组bytes  
                const charchar *bytes = NULL;  
                charchar *encoded;  

                dispatch_data_t md = dispatch_data_create_map(pipedata, (const voidvoid **)&bytes, &len);  
                encoded = asl_core_encode_buffer(bytes, len);  
                asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);  
                free(encoded);  
                _asl_send_message(NULL, merged_msg, -1, NULL);  
                asl_msg_release(merged_msg);  
                dispatch_release(md);  
            }  
        }  

        if (done)  
        {  
            dispatch_semaphore_signal(sem);  
            dispatch_release(pipe_channel);  
            dispatch_release(pipe_q);  
        }  

dispatch_io_create 函数生成Dispatch I/O,并指定发生error时用来执行处理的block,以及执行该block的Dispatch Queue。
dispatch_io_set_low_water 函数设置一次读取的大小
dispatch_io_read函数使用Global Dispatch Queue 开始并发读取。其中第四个参数是后面Block执行的队列,相当于函数是在全局队列中使用串行队列。(此处我也不是很明白,欢迎有其他见解的朋友发表看法)。每当各个分割的文件块读取结束时,将含有文件块数据的 Dispatch Data(这里指pipedata) 传递给 dispatch_io_read 函数指定的读取结束时回调用的block,这个block拿到每一块读取好的Dispatch Data(这里指pipe data),然后进行合并处理。

结语

  • 本文主要讲常用的一些GCD的API.
  • 此为《Objective-C 高级编程》的学习笔记。
  • 如有错误,欢迎指正。
  • 如需转载,请注明出处。

推荐阅读更多精彩内容