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取出任务执行,但是不会等待前一个任务完成就会取出第二个任务。


    SerialQueue.png
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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容

  • 一、简介在iOS所有实现多线程的方案中,GCD应该是最有魅力的,因为GCD本身是苹果公司为多核的并行运算提出的解决...
    MYS_iOS_8801阅读 571评论 0 0
  • 多线程学习笔记-GCD 我把这篇文章所用到的代码总结到这里->GCD项目总结下载地址-GCD-wxk可以下载参考 ...
    wxkkkkk阅读 532评论 0 2
  • 目录:iOS多线程(一)--pthread、NSThreadiOS多线程(二)--GCD详解iOS多线程(三)--...
    Claire_wu阅读 1,066评论 0 6
  • 一、基本概念 线程是用来执行任务的,线程彻底执行完任务A才能执行任务B,为了同时执行两个任务,产生了多线程 1、进...
    空白Null阅读 665评论 0 3
  • 为了能更好的传播产品经理精品文章,我们特意创建「PM 周刊」,将通过微信和邮件的形式推送给大家,每周周一定时推送。...
    四勾4J阅读 181评论 0 0