iOS多线程-GCD

1.基本介绍

什么是GCD

Grand Central Dispatch (GCD) 是异步执行任务的技术之一。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。

GCD的优势

  • GCD会自动的利用多核(比如双核,四核)
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • GCD会自动根据系统负载来增减线程数量

Dispatch Queues

GCD的基本概念就是dispatch queue。dispatch queue是一个对象,它可以接受任务,并将任务以先到先执行的顺序来执行。dispatch queue可以是并发的或串行的。并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行,串行队列同一时间只执行单一任务。

  • 串行队列:按FIFO每次取出一个任务执行,当前一个任务完成后才会取出第二个任务。
  • 并发队列:按FIFO取出任务执行,但是不会等待前一个任务完成就会取出第二个任务。


    iOS多线程-GCD_第1张图片
    SerialQueue.png
iOS多线程-GCD_第2张图片
ConcurrentQueue.png

任务

  • 同步任务:同步执行,会阻塞当前线程,直到当前的block任务执行完毕。
  • 异步任务:异步执行,不会阻塞当前线程。
队列与任务的组合情况
同步任务 异步任务
主队列 在主队列添加同步任务会死锁 不开启新线程,在主线程按序执行任务
串行队列 不开启新线程,在当前线程按序执行任务 开启一条线程,在这个线程中按序执行任务
并发队列/全局并发队列 不开启新线程,在当前线程按序执行任务 GCD根据系统资源开启多条线程执行任务

小结

  1. 同步和异步决定了是否开启新的线程。main队列除外,在main队列中,同步或者异步执行都不会另开线程。
  2. 串行和并行,决定了任务是否同时执行。
  3. 不要在执行串行队列的线程中向当前的串行队列添加同步任务,会导致死锁。

2.GCD使用

获取Dispatch Queue的方法有两种。
第一种方法是通过GCD的API生成Dispatch Queue。

dispatch_queue_t queue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);

第一个参数指定该队列的名称,该名称会出现应用程序崩溃时产生的CrashLog中,在调用过程中我们也可以使用dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)获取当前队列的名字。

第二个参数指定该队列的类型。

DISPATCH_QUEUE_SERIAL或者NULL表示该队列是串行队列,

DISPATCH_QUEUE_CONCURRENT表示该队列是并发队列。

第二种方法是获取系统标准提供的Dispatch Queue。

系统会给我们提供Main Dispatch Queue(主队列)和Global Dispatch Queue(全局并发队列)。

Main Dispatch Queue
Main Dispatch Queue顾名思义,是在主线程中执行的队列。因为主线程只有一个,所以主队列自然就是串行队列。追加到主队列的处理在主线程的RunLoop中执行,因此要将用户界面的更新等一些必须在主线程中执行的处理追加到主队列中使用。

// Main 队列获取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });

在上面说过,同步会阻塞当前线程,执行完block里面任务才会继续往下走。dispatch_sync阻塞了主线程,然后把任务追到加主队列,并在主线程执行,但是此时的主线程已经被阻塞,所以block任务也无法执行,block任务不执行,dispatch_sync会继续阻塞主线程。这样子就产生了死锁。

Global Dispatch Queue
Global Dispatch Queue是全局并发队列,没有必要通过dispatch_queue_create函数逐个生成并发,只要获取全局并发队列使用即可。
全局并发队列有4个执行优先级,分别是高优先级(High Priority),默认优先级(Default Priority),低优先级(Low Priority)和后台优先级(Background Priority)。

// 获取高优先级
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 获取默认优先级
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取低优先级
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
// 获取后台优先级
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue
dispatch_queue_create函数生成的队列不管是串行队列还是并发队列,都使用跟默认优先级的全局并发队列相同的优先级。而设置一个队列的优先级可以使用dispatch_set_target_queue。

dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serialQueue", NULL);

dispatch_queue_t backgroundGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue(serialQueue, backgroundGlobalQueue);

指定要变更优先级的队列为第一个参数,指定参考队列为第二个参数。

dispatch_set_target_queue函数还可以改变队列的执行层次。在多个串行队列中,使用dispatch_set_target_queue函数指定目标为某一串行队列,那么原本应并行执行的多个串行队列,在目标串行队列上只能同时执行一个任务。在必须要将不可并发执行的处理追加到多个串行队列中时,可以使用dispatch_set_target_queue函数防止并发执行。

    dispatch_queue_t serialQueue1 = dispatch_queue_create("serialQueue1", NULL);
    dispatch_queue_t serialQueue2 = dispatch_queue_create("serialQueue2", NULL);
    dispatch_queue_t serialQueue3 = dispatch_queue_create("serialQueue3", NULL);
    dispatch_queue_t serialQueue4 = dispatch_queue_create("serialQueue4", NULL);
    
    dispatch_set_target_queue(serialQueue2, serialQueue1);
    dispatch_set_target_queue(serialQueue3, serialQueue1);
    dispatch_set_target_queue(serialQueue4, serialQueue1);
    
    dispatch_async(serialQueue2, ^{
        NSLog(@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });
    dispatch_async(serialQueue3, ^{
        NSLog(@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });
    dispatch_async(serialQueue4, ^{
        NSLog(@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });

    //输出结果为
    serialQueue2
    serialQueue3
    serialQueue4

dispatch_after
想在指定时间后执行的情况,可使用dispatch_after函数来实现

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"等待3秒");
    });

使用dispatch_after需要注意的是,这个函数并不是在指定时间后执行,而是在指定时间把任务追加到指定的队列中。如上面的代码,3秒后只是把任务追加到主队列当中,具体执行时间与主线程拥塞程度有关。

Dispatch Group
我们经常会有这样的需求,在多个追加到Dispatch Queue中的处理执行完毕后,进行一些操作。在使用串行队列的时候,我们只需要在最后追加结束后的处理。但是在使用多个并发队列或同时使用多个队列时,就需要使用Dispatch Group。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"block1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block3");
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"done");
    });

    //执行结果
    block1
    block3
    block2
    done

很明显,这种方式是不阻塞的。由于我们是异步把任务添加到队列中,所以任务执行的顺序是不一定的。但是dispatch_group_notify里面的block肯定是最后执行。
如果想要阻塞线程可以使用dispatch_group_wait(group, DISPATCH_TIME_FOREVER);第二个参数为等待时间DISPATCH_TIME_FOREVER表示永远等待。

当任务中有completion block时,这种任务是马上完成的,例如网络请求。但是我们想让任务在收到completion block时才完成,这时需要我们手动管理任务的开始和结束。

    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    
    [Request requestWithSuccess:^{
        NSLog(@"success");
        dispatch_group_leave(group);
    } failBlk:^{
        NSLog(@"fail");
        dispatch_group_leave(group);
    }];
    
    
    dispatch_group_enter(group);
    
    [Request requestWithSuccess:^{
        NSLog(@"success");
        dispatch_group_leave(group);
    } failBlk:^{
        NSLog(@"fail");
        dispatch_group_leave(group);
    }];
    
    dispatch_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部完成");
    });

通过dispatch_group_enterdispatch_group_leave两个函数可以实现进入,退出两个动作。要注意enterleave要成对出现,否则group永远不会结束。

dispatch_barrier_async
在访问数据时,使用串行队列可以避免数据竞争问题。写入处理不可与其他的写入处理以及包含读取处理的其他某些处理并行执行,但是读取处理只和读取处理并行执行,那么多个并行执行就不会发生问题。也就是说,为了高效率的访问,需要实现多读单写。
GCD为我们提供了一种方便的实现dispatch_barrier_async,它等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行完成后,barrier函数之后的操作才会得到执行,该函数需要同dispatch_queue_create函数生成的并发队列一起使用。

    dispatch_queue_t queue = dispatch_queue_create("queueForBarrier", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任务1");
    });
    dispatch_async(queue, ^{
        NSLog(@"任务2");
    });
    dispatch_barrier_async(queue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任务3 barrier");
    });
    dispatch_async(queue, ^{
        NSLog(@"任务4");
    });
    dispatch_async(queue, ^{
        NSLog(@"任务5");
    });

    //输出结果
    任务2
    任务1
    任务3 barrier
    任务4
    任务5

dispatch_apply
dispatch_apply函数按指定的次数降block追加到指定的队列中,并阻塞线程等待全部处理执行结束。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSArray *array = @[@"1",@"2",@"3"];
    dispatch_apply(array.count, queue, ^(size_t index) {
        NSLog(@"%@",array[index]);
    });
    NSLog(@"完成");

    //输出结果
    2
    1
    3
    完成

由于dispatch_apply函数会等待所有处理执行结束,所以最好在dispatch_async函数中非同步的执行dispatch_apply函数

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSArray *array = @[@"1",@"2",@"3"];
    dispatch_async(queue, ^{
        dispatch_apply(array.count, queue, ^(size_t index) {
            NSLog(@"%@",array[index]);
        });
        
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主线程
        });
    });

dispatch_suspend/dispatch_resume

    //挂起指定的队列
    dispatch_suspend(queue);
    //恢复指定的队列
    dispatch_resume(queue);

dispatch_suspend函数对已经执行的处理没有影响,队列中未执行的处理会被挂起,dispatch_resume函数会恢复这些处理的执行。

Dispatch Semaphore
信号量可以控制同时访问资源的数量,解决资源争夺的问题。信号量持有计数,计数为0等待,计数大于或等于1时,减去1而不等待。类似于过安检时,允许n个人一起安检,每当一个人安检完成,则下一个人可以进行安检。

    //创建计数值为1的信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSArray *array = @[@"1",@"2",@"3",@"4",@"5"];
    for (NSString *string in array) {
        dispatch_async(queue, ^{
            //dispatch_semaphore_wait函数等待信号量的计数值大于或等于1时,对计数减1并向下执行,否则等待。
            //DISPATCH_TIME_FOREVER表示永远等待。
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

            [NSThread sleepForTimeInterval:1
            NSLog(@"%@",string);
            
            //dispatch_semaphore_signal函数将信号量的计数加1
            dispatch_semaphore_signal(semaphore);
        });
    }

上面代码创建了一个计数初始值为1的信号量,然后向全局并发队列中添加了5个任务,当有一个任务通过dispatch_semaphore_wait函数时,信号量的计数被减1,此时计数为0,剩余的任务则会等待,直到dispatch_semaphore_signal将计数加1。

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
    long result = dispatch_semaphore_wait(semaphore, time);
    if (result == 0) {
        //在等待时间内计数值达到大于等于1,减1继续处理
    }
    else{
        //在等待时间内计数值为0
    }

我们也可以通过dispatch_semaphore_wait函数的返回值进行分支处理。当我们给定一个等待时间,如果信号量的计数值在等待时间内达到大于等于1,dispatch_semaphore_wait返回0,如果超过这个时间计数值还为0,则返回值不为0。

dispatch_once
dispatch_once函数保证在应用程序执行中只执行一次处理。并且在多线程环境下能保证安全。

@implementation Manager

+ (Manager *)sharedInstance
{
    static Manage *manager = nil;
    static dispatch_once_t token;

    dispatch_once(&token, ^{
        manager = [[Manager alloc] init];
    });

    return manager;
}

我们一般用来创建单例。

Dispatch Source
Dispatch Source的种类

名称 种类
DISPATCH_SOURCE_TYPE_DATA_ADD 自定义事件,变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 自定义事件,变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH 端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH 端口接收
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ IO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL 接受信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE IO操作,如对文件的操作、socket操作的写响应

使用DISPATCH_SOURCE_TYPE_TIMER的定时器的例子

    //倒计时时间
    __block int timeout = 60;
    
    //创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    //创建timer
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    //设置开始时间未当前时间,间隔1秒,误差为0
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC), 0);
    
    //触发事件
    dispatch_source_set_event_handler(timer, ^{
        if (timeout <= 0) {
            //取消dispatch source
            dispatch_source_cancel(timer);
        }
        else{
            timeout --;
            NSLog(@"%d",timeout);
        }
    });
    
    //启动定时器
    dispatch_resume(timer);

你可能感兴趣的:(iOS多线程-GCD)