iOS中GCD的应用

GCD中的两个核心概念

  1. 任务
    任务指的是放入GCD中的操作,一般以Block的方式进行,执行任务的操作有两种
  • 同步执行:不会开启新的线程,在当前线程中执行,表现为同步函数sync
  • 异步执行:拥有开启新线程的执行任务的能力,表现为异步函数async
  1. 队列
    任务队列是用来存放任务的队列,采用先进先出的原则,队列也分为两种
  • 串行队列:队列中的任务一个接一个的执行,不会开启新的线程
  • 并发队列:在异步函数中会开启多条线程,同时执行任务

创建队列

  1. 创建串行队列
// 创建串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
  • 主队列也为一个特殊的串行队列,不需要创建,可以直接获取
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
  1. 创建并发队列
// 创建并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
  • 全局并发队列也不需要创建,可以直接获取
// 获取全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

创建任务

  1. 创建同步执行的任务
    // 同步函数+串行队列
    dispatch_sync(serialQueue, ^{
        // 不会开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
    // 同步函数+主队列
    dispatch_sync(mainQueue, ^{
        // 不会开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
    // 同步函数+并发队列
    dispatch_sync(concurrentQueue, ^{
        // 不会开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
  1. 创建异步执行的任务
    // 异步函数+并发队列
    dispatch_async(concurrentQueue, ^{
        // 开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
    // 异步函数+串行队列
    dispatch_async(serialQueue, ^{
        // 开启一条后台线程,串行执行任务
        NSLog(@"%@",[NSThread currentThread]);
    });
    // 异步函数+主队列
    dispatch_async(mainQueue, ^{
        // 不开起新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });

代码验证

  • 串行队列+同步函数
#pragma mark - 串行队列+同步函数
- (void)configSyncSerialQueue {
    NSLog(@"开始执行");
    NSArray *titleArray = @[@"第一个任务",@"第二个任务",@"第三个任务"];
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue""", DISPATCH_QUEUE_SERIAL);
    for (NSString *str in titleArray) {
        dispatch_sync(serialQueue, ^{
            NSLog(@"串行队列+同步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
    
    for (NSString *str in titleArray) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"主队列+同步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
}
Snip20180118_4.png

没有开启新的线程,均在主线程中执行

  • 串行队列+异步函数
#pragma mark - 串行队列+异步函数
- (void)configAsyncSerialQueue {
    NSLog(@"开始执行");
    NSArray *titleArray = @[@"第一个任务",@"第二个任务",@"第三个任务"];
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue""", DISPATCH_QUEUE_SERIAL);
    for (NSString *str in titleArray) {
        dispatch_async(serialQueue, ^{
            NSLog(@"串行队列+异步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
    
    for (NSString *str in titleArray) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"主队列+异步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
}
Snip20180118_5.png

可以发现,串行队列+异步函数的组合开启了新的线程,而主队列+异步函数确没有开启新的线程,任在主线程执行任务.

#pragma mark - 并发队列+同步函数
- (void)configSyncConcurrentQueue {
    NSLog(@"开始执行");
    NSArray *titleArray = @[@"第一个任务",@"第二个任务",@"第三个任务"];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue""", DISPATCH_QUEUE_CONCURRENT);
    for (NSString *str in titleArray) {
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"并发队列+同步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
    
    for (NSString *str in titleArray) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"全局并发队列+同步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
}
Snip20180118_6.png

可见,同步函数不具有开启新线程的能力

#pragma mark - 并发队列+异步函数
- (void)configAsyncConcurrentQueue {
    NSLog(@"开始执行");
    NSArray *titleArray = @[@"第一个任务",@"第二个任务",@"第三个任务"];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue""", DISPATCH_QUEUE_CONCURRENT);
    for (NSString *str in titleArray) {
        dispatch_async(concurrentQueue, ^{
            NSLog(@"并发队列+异步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
    
    for (NSString *str in titleArray) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"全局并发队列+异步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
}
Snip20180118_7.png

开启了新的线程,任务并发执行

注意:在使用过程中需要注意死锁问题!

所谓死锁,通常指有两个线程A和B都卡住了,A在等B ,B在等A,相互等待对方完成某些操作。A不能完成是因为它在等待B完成。但B也不能完成,因为它在等待A完成。于是大家都完不成,就导致了死锁

#pragma mark - 死锁案例
- (void)deadLock {
    NSLog(@"任务1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
}
  • 任务1,2,3都加入到了主队列中
  • 同步函数+主队列不会开启新的线程
  • 所有任务都在主线程执行
    首先执行任务1,接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3.但这是主队列,是一个特殊的串行队列,有任务来,会将任务加到队尾,然后遵循FIFO原则执行任务.那么,现在任务2就会被加到最后,任务3排在了任务2前面.但是任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面,程序就卡在了这里,这就是死锁.

GCD的具体应用

  • 线程间通信
    在iOS开发中,主线程有称为UI线程,用来处理UI事件.其他耗时操作通常放在子线程中进行,比如网络请求等.通过网络请求回来的数据通常需要用UI展示,这是就需要从子线程回到主线程,进而就产生了线程间通信.
#pragma mark - 线程间通信
- (void)GCDCommunication {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 全局并发队列中异步请求数据
        NSArray *dataArray = @[@"我是第1条数据",@"我是第2条数据",@"我是第3条数据"];
        for (NSString *dataStr in dataArray) {
            NSLog(@"%@---我当前的线程是:%@",dataStr,[NSThread currentThread]);
        }
        // 请求数据完成,回到主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"我当前的线程是:%@",[NSThread currentThread]);
        });
    });
}
Snip20180118_8.png

可以看到,在子线程中请求完数据之后又回到了主线程中执行任务

  • GCD定时器
    首先NSTimer可以做定时器,但是它受到RunLoop的Mode影响,不是特别准确,而且容易造成循环引用的问题.GCD定时器可以规避这些问题.
int count = 0;
- (void)GCDTimer {
    self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC); // 从现在开始两秒后执行
    dispatch_source_set_timer(self.gcdTimer, startTime, (int64_t)(2.0 * NSEC_PER_SEC), 0); // 每两秒执行一次
    // 定时器回调
    dispatch_source_set_event_handler(self.gcdTimer, ^{
        NSLog(@"CGD定时器-----%@",[NSThread currentThread]);
        count++;
        if (count == 5) { // 执行5次,让定时器取消
            dispatch_cancel(self.gcdTimer);
            self.gcdTimer = nil;
        }
        
    });
    // 启动定时器: GCD定时器默认是暂停的
    dispatch_resume(self.gcdTimer);
}
  • GCD控制并发
    GCD中控制并发可以通过
    dispatch_group 队列组
    dispatch_barrier 栅栏函数
    dispatch_semahpore 信号量
    来控制
    1.dispatch_group
    在某些特殊的场景下我们需要同时执行多个耗时任务,并且在多个任务都完成的之后在回到主线程刷新UI,此时就可以使用dispath_group了
#pragma mark - configDispatch_group
- (void)configDispatch_group {
    dispatch_group_t gcdGroup = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(gcdGroup, queue, ^{
        NSLog(@"执行第一个任务");
    });
    dispatch_group_async(gcdGroup, queue, ^{
        NSLog(@"执行第二个任务");
    });
    dispatch_group_async(gcdGroup, queue, ^{
        NSLog(@"执行第三个任务");
    });
    dispatch_group_notify(gcdGroup, dispatch_get_main_queue(), ^{
        NSLog(@"回到了主线程");
    });
}
Snip20180118_9.png

在执行完了任务1,2,3之后回到了主线程,起到了控制的作用

  1. dispatch_barrier 栅栏函数
    当一个任务的执行与否依赖于上一个任务执行的结果的时候我们可以使用dispatch_barrier,栅栏函数的作用在于控制并发任务执行的先后顺序
#pragma mark - dispatch_barrier
- (void)configDispatch_barrier {
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"执行第一个任务--%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"执行第二个任务--%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"执行第三个任务--%@",[NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"我是栅栏,前边的任务都执行完了,在执行下边的任务--%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"执行第四个任务--%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"执行第五个任务--%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"执行第六个任务--%@",[NSThread currentThread]);
    });
}
Snip20180118_10.png

注意:这里的dispatch_queue_t queue中的queue必须为dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT)创建的,使用全局并发队列不起作用

  1. dispatch_semahpore 信号量
    有时候在开发中我们希望执行完一个任务(成功/失败)才接着执行下一个任务,这是我们可以使用信号量来控制
    信号量运行准则:
    信号量就是一个资源计数器,当其不为0时线程正常运行,为0时则阻塞当前线程.
    实现原理:
    使用信号量的PV操作来实现互斥.P:信号量-1,V:信号量+1
    例如:
    默认初始信号量为1
    当A正常运行,使用资源;对信号量实施P操作,信号量-1
    当B期望使用资源来正常运行,发现信号量为0(阻塞),B一直等待
    A执行完成,进行V操作,释放资源,信号量+1
    B检测到信号量不为0,则正常运行
#pragma mark - 信号量
- (void)configDispatch_semaphore {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 创建信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    NSArray *titleArray = @[@(1),@(1),@(1),@(0),@(1)];
    for (int i = 0; i<titleArray.count; i++) {
        int number = [titleArray[i] intValue];
        dispatch_async(queue, ^{
            //信号量为0是则阻塞当前线程,不为0则减1继续执行当前线程
            dispatch_wait(semaphore, DISPATCH_TIME_FOREVER);
            if (number) {
                NSLog(@"%d--当前线程:%@",i,[NSThread currentThread]);
                dispatch_semaphore_signal(semaphore);
            } else {
                NSLog(@"%d--当前线程:%@",i,[NSThread currentThread]);
                dispatch_semaphore_signal(semaphore);
            }
        });
    }
}
Snip20180118_11.png

可见,信号量起到了控制任务执行顺序的作用

  • GCD延时函数
#pragma mark - GCD延时函数
- (void)configDispatch_after {
    NSLog(@"开始执行");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"两秒后我执行了");
    });
}
  • GCD一次性代码
    通常用于创建单列
#pragma mark - GCD一次性代码
- (void)configDispatch_once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 这里写只需要执行一次的代码,默认线程安全
    });
}
  • GCD快速迭代(遍历)
    GCD快速遍历与for,while循环不同,它会开启新的线程来遍历(在并发队列中)
- (void)configDispatch_apply {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_apply(15, queue, ^(size_t index) {
        NSLog(@"执行第%ld个任务,当前线程为%@",index,[NSThread currentThread]);
    });
}
Snip20180118_12.png

可以看出,GCD快速迭代是在不同线程中执行的,而且还包含了主线程.

注意,这里的快速迭代是在并发队列中执行的,如果放在串行队列中,操作会在主线程中执行,没有起到快速迭代的作用

- (void)configDispatch_apply {
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_queue_t queue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_apply(15, queue, ^(size_t index) {
        NSLog(@"执行第%ld个任务,当前线程为%@",index,[NSThread currentThread]);
    });
}
Snip20180118_13.png

同时应该注意,这里不要使用主队列,在快速迭代使用的线程中包括了主线程,会造成死锁现象

以上内容就是我在iOS开发过程中所理解和使用到的GCD相关内容,如有严谨的地方请读者纠正,谢谢!
以上代码demo地址:https://github.com/geekGetup/GCD

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

推荐阅读更多精彩内容