iOS多线程-GCD

1、字面理解
GCD全称Grand Central Dispatch,是苹果提供的一个多核编程的解决方案,在真正意义上实现了并行操作,而不是并发。

GCD使用线程池模型来执行用户提交的任务,所以它比较节约资源,不需要为每个任务都重新创建一个新的线程,GCD不需要自行编写并行代码,而是自动进行多核的并行计算,自动管理线程的生命周期,如:使用线程池管理线程的创建和销毁,线程的调度,任务的调度等,用户只需要编写任务代码并提交即可。

GCD中有两个比较重要的概念:任务和队列

  • GCD的任务
    任务就是我们需要执行的的代码,它可以是一个方法也可以是一个block,实际上就是我们需要线程为我们完成的工作。我们把任务提交给GCD的队列,GCD就可以自动完成任务的调度,线程的调度,也很方便的实现了多线程并发执行任务。

  • GCD的队列
    队列是用于管理用户提交的任务,它有两种形式:串行队列和并行队列。
    串行队列: GCD底层只维护一个线程,任务只能串行依次执行。
    并发队列: GCD底层使用线程池维护多个线程,任务可并行或并发执行。

无论是并行队列还是串行队列,都是按先进先出FIFO的方式来处理用户提交的任务。

GCD里,我们只需要创建队列或者获取系统队列,编写任务代码,并把任务提交给队列就可以了。

2、GCD获取队列的方法

/*获取系统队列*/

/*
获取主队列,即与主线程相关联的队列
如果需要提交任务到主线程使用该方法获取主线程的主队列即可
主队列是串行队列因为只维护主线程一个线程
*/
dispatch_queue_t dispatch_get_main_queue(void)

/*
获取一个全局的并发队列
identifier指定该队列的优先级可选值有:
    DISPATCH_QUEUE_PRIORITY_HIGH 2
    DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    DISPATCH_QUEUE_PRIORITY_LOW (-2)
    DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags 是苹果预留参数,未来可能用的到,现在只传0就可以了
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags)

/*用户创建队列*/

/*
创建一个队列
label 队列的名称
attr 队列的属性可选值有:
    DISPATCH_QUEUE_SERIAL 创建一个串行队列
    DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
通过这种方式可以自己维护一个队列
*/
dispatch_queue_t dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr)

3、队列获取示例

   //串行主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //全局并行队列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //自建并行队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //自建串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);

4、 向队列提交任务的方法

/*
以异步方式执行任务,不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

/*
同上
context 是一个void*的指针,作为work的第一个形参
work 是一个函数指针,指向返回值为void 形参为void*的函数,且形参不能为NULL,也就是说context一定要传
使用起来不方便,一般不怎么用,需要使用C函数,也可以使用OC方法通过传递IMP来执行但是会有编译警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式执行任务,阻塞当前线程,必须等待任务完成当前线程才可继续执行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任务,并重复执行iterations次
iterations 迭代执行次数
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值为void形参为iterations迭代次数
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue,  DISPATCH_NOESCAPE void (^block)(size_t));

//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));

/*
以异步方式提交任务,在when时间点提交任务
queue 管理任务的队列,任务最终交由该队列来执行
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以异步方式提交任务,会阻塞queue队列,但不阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
需要说明的是,即时使用并发队列,该队列也会被阻塞,前一个任务执行完成才能执行下一个任务
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任务,会阻塞queue队列,也会阻塞当前线程
queue 管理任务的队列,任务最终交由该队列来执行
同样的,即时是并发队列该队列也会被阻塞,需要等待前一个任务完成,同时线程也会阻塞
block block形式的任务,该block返回值、形参都为void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
底层线程池控制block任务在整个应用的生命周期内只执行一次
predicate 实际为long类型,用于判断是否执行过
block block形式的任务,该block返回值、形参都为void
该方法常用于实现单例类,以及结合RunLoop创建一个常驻内存的线程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);


5、异步提交任务到并发队列

//自建并行队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //异步提交任务到并发队列
    dispatch_async(concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 50) {
            i++;
            NSLog(@"task1: %ld thread:%@",i,NSThread.currentThread);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 50) {
            i++;
            NSLog(@"task2: %ld thread:%@",i,NSThread.currentThread);
        }
    });
    
    NSInteger indexTag = 10;
    //调用C函数
    dispatch_async_f(concurrentQueue, &indexTag, cFunctionDispatch);
    /*
    //oc转换函数
    dispatch_async_f(concurrentQueue,&indexTag,[self methodForSelector:@selector(ocFunctionDispatch:)]);
    */

从控制台可以看到异步执行提交任务到并行队列,任务是分配到不同的线程的,并发执行。

6、异步提交任务到串行队列

//异步提交任务到串行队列 任务是在同一个线程执行,前一个任务执行完之后,后一个任务才会执行。
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task5: %ld thread:%@",i,NSThread.currentThread);
        }
    });
    dispatch_async(serialQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task6: %ld thread:%@",i,NSThread.currentThread);
        }
    });

从控制台打印的数据可以看到,串行队列的任务是分配到同一个线程执行,遵循FIFO规则。前一个任务完成之后,后一个任务才会被调度执行。
7、同步提交任务到并发队列

//同步提交任务掉并发队列
    dispatch_sync(concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task7: %ld thread:%@",i,NSThread.currentThread);
        }
    });
    dispatch_sync(concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task8: %ld thread:%@",i,NSThread.currentThread);
        }
    });
    dispatch_sync(concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task9: %ld thread:%@",i,NSThread.currentThread);
        }
    });
  dispatch_async(concurrentQueue, ^{
        dispatch_sync(concurrentQueue, ^{
            NSInteger i = 0;
            while (i < 10) {
                i++;
                NSLog(@"task10: %ld thread:%@",i,NSThread.currentThread);
            }
        });
    });

从输出可以看出,前三个任务都是用主线程来执行,最后一个在number=6的线程里执行的,按照并发队列的特性,这里的三个任务完全可能由不同的三个线程来执行。经测试,同步执行任务阻塞线程,当前队列在哪个线程里,当前任务就会被调度到哪个线程执行。并且任务的执行先后顺序是FIFO原则。
8、同步提交任务到串行队列
这个就不测试了。同步执行阻塞当前线程,同时串行执行,由当前线程执行任务。都是先进先出的规则FIFO

8、dispatch_barrier _ (a)sync

- (void)viewWillAppear:(BOOL)animated
{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task0 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
        }
    });
}


上面的输出是按照Task0 Task1并发执行,Task2等待Task0 Task1执行完成后单独执行, 最后Task3 Task4等待Task2执行完成后开始并发执行 。

这里需要讲解一下阻塞队列的概念,前文讲过不论是并发队列还是串行队列都是使用FIFO先进先出的方式管理的,队列会从队首获取要执行的任务并交由对应线程处理,串行队列只有一个线程所以是顺序执行,并发队列有多个线程,但获取任务依旧是FIFO按顺序获取,只是执行时有多个线程。阻塞线程即,获取一个任务后,这个任务必须要执行完成才能获取下一个任务,所以不管是并发还是串行队列,都得等前一个任务完成了才能从队列中获取下一个任务,这样就不难理解输出结果了,上述栗子改成串行队列结果也是一样的,如果使用同步提交效果也是一样的,读者可以自行尝试,篇幅问题不再赘述了。

dispatch_barrier_async方法常与并发队列共用,前一段任务使用dispatch_async异步并发执行,然后插入一个dispatch_barrier_async执行一个中间任务,这个中间任务必须要等待前面的并发任务执行完成后才能开始执行,接着这个中间任务完成后,继续异步并发执行接下来的任务。

9、dispatch_once

该方法能够保证在应用的生命周期内只执行一次提交的任务,所以常用于单例类的创建,举个单例类的栗子如下:

@interface MyUtil: NSObject <NSCopying>

+ (instancetype)sharedUtil;

@end

@implementation MyUtil

static MyUtil *staticMyUtil = nil;

+ (instancetype)sharedUtil
{
    //保证初始化创建只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticMyUtil = [[MyUtil alloc] init];
    });
    return staticMyUtil;
}

//防止通过alloc或new直接创建对象
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    //保证alloc函数只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticMyUtil = [super allocWithZone:zone];
    });
    return staticMyUtil;
}

//实现NSCopying协议的方法,防止通过copy获取副本对象
- (instancetype)copyWithZone:(NSZone *)zone
{
    return staticMyUtil;
}

@end

dispatch_once函数需要传入一个long类型的predicate,这个值必须是独一无二的,使用静态变量的地址最合适不过了,MyUtil实现了NSCopying协议的copyWithZone:方法,防止通过copy方法获取副本对象。

当使用alloc&&init方法初始化时,先调用allocWithZone:方法来分配存储空间,如果再次使用sharedUtil方法来获取的话,由于没有执行过,会执行到dispatch_once内部block,此时会再去执行allocWithZone:方法,但该方法内部dispatch_once已经执行过了会直接返回staticMyUtil,反过来调用是一样的道理,通过这样的方式就可以实现真正的单例了。

10、dispatch_group_t
dispatch_group_t是一个比较实用的方法,通过构造一个组的形式,将各个同步或异步提交任务都加入到同一个组中,当所有任务都完成后会收到通知,用于进一步处理,通过这样的方式就可以实现多线程下载,当下载完成后就可以通知用户了,举个简单的栗子如下:

dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);//enter和leave group可以保证三个任务都执行完之后再调用dispatch_group_notify的回调
    dispatch_group_async(group, concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task11: %ld thread:%@",i,NSThread.currentThread);
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        
        dispatch_async(concurrentQueue, ^{
            sleep(2);
            NSInteger i = 0;
            while (i < 10) {
                i++;
                NSLog(@"task12: %ld thread:%@",i,NSThread.currentThread);
            }
            dispatch_group_leave(group);
        });
        
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, concurrentQueue, ^{
        NSInteger i = 0;
        while (i < 10) {
            i++;
            NSLog(@"task13: %ld thread:%@",i,NSThread.currentThread);
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, concurrentQueue, ^{
        NSLog(@"group task completed!");
    });

三个异步任务完成之后,发出通知调用block,输出:group task completed!

祭出demo

参考:

https://www.jianshu.com/p/e9d8a087f6c0

更新时间2018-08-24

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

推荐阅读更多精彩内容

  • 一顿外卖实际支付7块钱,还给送上门,这不是广告,真实发生的,在饿xx平台缴费注册一个会员,那么每单五块的配送费省了...
    Bonew01阅读 221评论 0 0
  • 早晨无意间翻看到恶作剧之吻的图片,湘琴和直树的故事又历历在目了,好怀念当时通宵看这部剧的心情。哼哧哼哧地一晚上将第...
    啾啾fing阅读 182评论 0 0
  • 我叫云惜儿,是这冥渊境中唯一的主,他们都羡慕我高高在上的权位,却不曾想,我不爱,...
    言惜惜阅读 396评论 1 2