OC高级-GCD使用总结

GCD简介

  • GCD全称:Grand Central Dispatch,译为大型的中枢调度器
  • 纯C语言实现,提供了非常多强大的功能
  • GCD的优势
    • GCD是苹果公司为多核的并行运算提出的解决方案
    • GCD会自动利用更多的CPU内核(如:双核、四核)
    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
    • 程序猿只需要告诉GCD想要执行什么任务,不需要编写任何管理线程的代码
  • GCD的两个核心概念:任务队列
    • 任务:执行什么操作
    • 队列:用来存放任务,分为:并行队列和串行队列

并行队列 串行队列

  • 队列本质:用于控制任务的执行方式
  • 并行队列
    • 英文:Concurrent Dispatch Queue
    • 可以让多个任务并发执行,以提高执行效率
    • 并发功能异步(dispatch_async)函数下才有效
  • 串行队列
    • 英文:Serial Dispatch Queue
    • 在当前线程中让任务一个接着一个地执行
  • 创建队列
// 队列类型
dispatch_queue_t

// 第一个参数:队列名称
// 第二个参数:队列类型
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

/** 队列类型
  // 串行队列标识:本质就是NULL,但建议不要写成NULL,可读性不好
  DISPATCH_QUEUE_SERIAL
  // 并行队列标识
  DISPATCH_QUEUE_CONCURRENT
*/
  • 创建并行队列的两种方式
    • 直接创建
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT);
    
    • 获取全局并发队列
    // 第一个参数:队列优先级
    // 第二个参数:保留参数,暂时无用,用0即可
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 全局并发队列的优先级
    #define DISPATCH_QUEUE_PRIORITY_HIGH 2               // 高
    #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0            // 默认(中)
    #define DISPATCH_QUEUE_PRIORITY_LOW (-2)             // 低
    #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台
    
  • 创建串行队列的两种方式
    • 直接创建
    // 创建串行队列(队列类型传递DISPATCH_QUEUE_SERIAL或者NULL)
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL);
    
    • 获取主队列:主队列是一种特殊的串行队列
    // 主队列中的任务,都会放到主线程中执行
    dispatch_queue_t queue = dispatch_get_main_queue();
    

同步(sync)函数 异步(async)函数

  • 函数作用:将任务添加到队列
  • 函数类型:决定是否有开启新线程的能力
  • 同步函数:不具备开启新线程的能力,只能在当前线程中执行任务
// queue:队列
// block:任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
  • 异步函数:具备开启线程的能力,但不一定开启新线程,比如:当前队列为主队列,异步函数也不会开启新的线程
// queue:队列
// block:任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
  • 经验总结:
    • 通过异步函数添加任务到队列中,任务不会立即执行
    • 通过同步函数添加任务到队列中,任务立即执行

程序猿只需要做下列事情

  • 指定函数类型:是否具备开启新线程的能力
  • 指定队列类型:决定任务的执行方式
  • 确定要执行的任务,并通过函数任务添加到队列中,任务的执行遵循队列的FIFO原则:先进先出,后进后出
  • 剩下的事情就交给GCD来完成了

函数和队列组合后的执行效果

Thread_3.png
  • 异步函数 + 并行队列
    • 开启多条子线程,任务并行执行
    • 代码展示
- (void)asyncConcurrent
{
    // 1.创建并行队列
//    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 2.通过异步函数将将任务加入队列
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<10; i++) {
            NSLog(@"1-----%@", [NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<10; i++) {
            NSLog(@"2-----%@", [NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<10; i++) {
            NSLog(@"3-----%@", [NSThread currentThread]);
        }
    });

    // 证明:异步函数添加任务到队列中,任务【不会】立即执行
    NSLog(@"asyncConcurrent--------end");

    // 释放队列,ARC中无需也不允许调用这个方法
//    dispatch_release(queue);
}
  • 异步函数 + 串行队列
    • 开启一条子线程,任务是有序的在子线程上执行
    • 代码展示
- (void)asyncSerial
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL);
//    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", NULL);

    // 2.通过异步函数将任务加入队列
    dispatch_async(queue, ^{
        NSLog(@"1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"2-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3-----%@", [NSThread currentThread]);
    });

    // 证明:异步函数添加任务到队列中,任务【不会】立马执行
    NSLog(@"asyncConcurrent--------end");
}
  • 异步函数 + 主队列
    • 不开启子线程,任务是在主线程中有序执行
    • 代码展示
- (void)asyncMain
{
    // 1.获得主队列
    dispatch_queue_t queue = dispatch_get_main_queue();

    // 2.通过异步函数将任务加入队列
    dispatch_async(queue, ^{
        NSLog(@"1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"2-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"3-----%@", [NSThread currentThread]);
    });
}
  • 同步函数 + 并行队列
    • 不会开启子线程,任务是有序执行
    • 代码展示
- (void)syncConcurrent
{
    // 1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 2.通过同步函数将任务加入队列
    dispatch_sync(queue, ^{
        NSLog(@"1-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"2-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"3-----%@", [NSThread currentThread]);
    });

    // 证明:同步函数添加任务到队列中,任务【立马执行】
    NSLog(@"syncConcurrent--------end");
}
  • 同步函数 + 串行队列
    • 不会开启线程,任务是有序执行
    • 易发生死锁,使用时要注意
    • 下面的用法会发生死锁吗?
- (void)syncMain
{
    // 1.创建串行队列
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL);

    // 2.将任务加入队列
    dispatch_sync(queue, ^{
        NSLog(@"1-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"2-----%@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"3-----%@", [NSThread currentThread]);
    });
}
  • 答案:上面的用法不会发生死锁,原因分析如下:
    • 虽然都是在主线程上执行的,但任务在不同的队列中所以不会发生阻塞
    • syncMain函数是在主队列中
    • 其他的任务是在新建的串行队列
  • 死锁的几中场景
    • 场景1
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
           NSLog(@"1-----%@", [NSThread currentThread]);
    
           // 这里阻塞了
           dispatch_sync(queue, ^{
                 NSLog(@"2-----%@", [NSThread currentThread]);
           });
    });
    
    • 场景2
      - (void)syncMain
      {
          // 获得主队列
          dispatch_queue_t queue = dispatch_get_main_queue();
    
          // 这里阻塞了
          dispatch_sync(queue, ^{
              NSLog(@"1-----%@", [NSThread currentThread]);
          });
          dispatch_sync(queue, ^{
              NSLog(@"2-----%@", [NSThread currentThread]);
          });
          dispatch_sync(queue, ^{
              NSLog(@"3-----%@", [NSThread currentThread]);
          });
      }
    
    • 原因分析
      • 使用同步函数在任务执行过程中往任务所在的串行队列中添加任务就会导致任务间互相等待,造成死锁
      • 别忘了同步函数添加任务到队列中,任务会立即执行,如果是异步函数不会发生死锁

GCD实现线程间通信

// 全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 异步函数
dispatch_async(queue, ^
{
        // 执行耗时的任务
        coding...

        // 【标记1】回到主线程,执行UI刷新操作
        dispatch_async(dispatch_get_main_queue(), ^
        {
            coding...

            // 还可以嵌套:再回到子线程做其他事情
            dispatch_async(queue, ^
            {
                coding...
            });
        });

        // 后续代码
        coding....
});
  • 【标记1】处也可以用同步函数回到主线程,但是同步函数会导致添加的新任务立即执行,导致必须等添加到主队列的任务执行完才会继续执行,也不是不能这么用,看具体场景是否需要等待主队列的任务执行完毕才继续往后执行

GCD中其他常用函数

dispatch_once_t

  • 函数作用:保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这个函数本身是【线程安全】的)
});

dispatch_after和dispatch_time_t

  • 函数作用:延迟将任务提交到队列中,不要理解成延迟执行任务
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
dispatch_after(time, queue, ^
{
    // 此任务被延迟提交到队列中
});
  • dispatch_time_t
    • 第一个参数一般是DISPATCH_TIME_NOW,表示从现在开始
    • 第二个参数就是真正的延时时间,单位为纳秒
    • 关于NSEC_PER_SEC的解释可以查看我这篇文章

dispatch_suspend 和 dispatch_resume

  • dispatch_suspend
    • 函数作用:只能挂起队列中还未执行的任务,正在运行的任务是无法挂起的
  • dispatch_resume
    • 函数作用:只能恢复队列中还未执行的任务

dispatch_apply

  • 此函数和dispatch_sync函数一样,会等待处理结束,所以建议在dispatch_async中使用此函数
  • 此函数必须结合并行队列才能发挥作用
  • 函数作用:可以快速完成对顺序没有要求的集合遍历,因为执行顺序不确定
  • 使用说明
dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index)
{
    // 执行10次代码,会开启多条线程来执行任务,执行顺序不确定
});
  • 示例:文件剪切
- (void)applyDemo
{
    NSString *from = @"/Users/xxx/Desktop/From";
    NSString *to = @"/Users/xxx/Desktop/To";

    NSFileManager *mgr = [NSFileManager defaultManager];
    NSArray *subpaths = [mgr subpathsAtPath:from];

    // 并行队列才会起作用
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(subpaths.count, queue, ^(size_t index) {
        NSString *subpath = subpaths[index];
        NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
        NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
        // 剪切
        [mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil];

        NSLog(@"%@---%@", [NSThread currentThread], subpath);
    });
}

dispatch_barrier_async

  • 必须并行队列,且不能使用全局的并行队列,实践证明不管用
  • 函数作用:在此函数前面的任务执行完成后此函数才开始执行,在此函数后面的任务等此函数执行完成后才会执行
- (void)barrierDemo
{
    //【不能】使用全局并发队列
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });

    // 在它前面的任务执行结束后它才执行,在它后面的任务等它执行完成后才会执行
    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}

dispatch_group

  • 必须是并行队列才起作用
  • 需求描述
    • 现有三个任务:任务A、任务B、任务C
    • 任务C需要等到任务A和任务B都完成后才执行
    • 任务A和任务B执行没有先后顺序
  • 使用dispatch_group可以实现上面的需求
  • 创建dispatch_group_t
// 创建队列组
dispatch_group_t group =  dispatch_group_create();
  • 添加任务分两种情况
    • 自己可以控制并创建队列,使用dispatch_group_async
    // 省去创建group、queue代码......
    dispatch_group_async(group, queue, ^{
        // 添加任务A到group
    });
    
    dispatch_group_async(group, queue, ^{
        // 添加任务B到group
    });
    
    • 无法控制队列,即使用的队列不是你创建的(如:AFNetworking异步添加任务),此时可以使用dispatch_group_enterdispatch_group_leave控制任务的执行顺序
    // 使用dispatch_group_enter,dispatch_group_leave可以方便的将一系列网络请求打包起来
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    
    // 添加任务A到group
    // ---打标记---
    dispatch_group_enter(group);
    [manager GET:@"http://www.baidu.com" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
      // do something
    
      // ---删除标记---
      dispatch_group_leave(group);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
      // do something
    
      // ---删除标记---
      dispatch_group_leave(group);
    }];
    
    // 添加任务B到group类似上面的操作
    
  • 添加结束任务也分为两种情况
    • dispatch_group_notify(推荐):不会阻塞当前线程,马上返回
    dispatch_group_notify(group, dispatch_get_main_queue(), ^
    {
        // do something
    });
    
    • dispatch_group_wait(不推荐):阻塞当前线程,直到dispatch group中的所有任务完成才会返回
    // 第二个参数是超时时间
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
  • 完整示例
// 创建队列组
dispatch_group_t group =  dispatch_group_create();

// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 添加任务A到group
dispatch_group_async(group, queue, ^{
    // 添加任务A到group
});

// 添加任务B到group
dispatch_group_async(group, queue, ^{
    // 添加任务B到group
});

// 当任务A和任务B都执行完后到此来执行任务C
dispatch_group_notify(group, queue, ^{
    // 如果这里还有基于上面两个任务的结果继续执行一些代码,建议还是放到子线程中,等代码执行完毕后在回到主线程

    // 回到主线程
    dispatch_async(group, dispatch_get_main_queue(), ^{
        // 执行相关UI显示代码...
    });
});

dispatch_set_context与dispatch_set_finalizer_f的配合使用

  • 函数作用:为队列设置任意类型的数据,并在合适的时候取出来用
  • 函数定义
// 设置context
void dispatch_set_context(dispatch_object_t object, void *context);

// 获取context
void* dispatch_get_context(dispatch_object_t object);
  • 参数介绍
    • 第一个参数object:一般指通过dispatch_queue_create创建的队列
    • dispatch_set_context函数完成了将context绑定到指定的GCD队列上
    • dispatch_get_context函数完成了从指定的GCD队列获取对应的context
    • context是一个void类型指针,学过C语言的朋友应该都知道,void类型指针可以指向任意类型,context在这里可以是任意类型的指针
  • 补充:Foundation对象Core Foundation对象间的转换,俗称桥接,请查看这篇文章
  • 完整示例
@interface Data : NSObject

@property(assign, nonatomic) int number;

@end

@implementation Data

// 便于观察对象何时被释放
- (void)dealloc 
{
    NSLog(@"Data dealloc...");
}

@end

-----------------------------------------------------------------------------------------

// 定义队列的finalizer函数,用于释放context内存
void cleanStaff(void *context) {
    // 这里用__bridge转换,不改变内存管理权
    Data *data = (__bridge Data *)(context);
    NSLog(@"In clean, context number: %d", data.number);

    // 释放context的内存!
    CFRelease(context);
}

- (void)testBody 
{
    // 创建队列
    dispatch_queue_t queue = dispatch_queue_create("yanhooQueue", DISPATCH_QUEUE_CONCURRENT);

    // 创建Data类型context数据并初始化
    Data *myData = [Data new];
    myData.number = 10;

    // 绑定context
    // 这里用__bridge_retained将OC对象转换为C对象,将context的内存管理权从ARC移除,交由我们自己手动释放!
    dispatch_set_context(queue, (__bridge_retained void *)(myData));

    // 设置finalizer函数,用于在队列执行完成后释放对应context内存
    dispatch_set_finalizer_f(queue, cleanStaff);

    dispatch_async(queue, ^
   {
        // 获取队列的context数据
        // 这里用__bridge将C对象装换为OC对象转换,并没有改变内存管理权
        Data *data = (__bridge Data *)(dispatch_get_context(queue));
        // 打印
        NSLog(@"1: context number: %d", data.number);
        // 修改context保存的数据
        data.number = 20;
    });
}

参考文章

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

推荐阅读更多精彩内容