iOS开发——多线程GCD入门

0.前言

0.什么是GCD?

GCD全程为Grand Central Dispatch,是异步执行的技术之一。一般将应用程序中记述的线程管理代码在系统级实现。开发者只用定义向执行的任务,并追加到适当的Dispatch Queue中就完事儿了,GCD能自动生成必要的线程并执行任务。

1.单线程

CPU一次只能执行一个命令 ,不能一心两用,同时执行两个分开的并列命令,因此通过CPU执行命令就像是一条五分叉的路,及时地址分散,看似凌乱,实则也只有一条路径。这就是线程。

2.上下文切换

macOS和iOS中的核心XNU内核在发生操作系统事件时,会切换执行路径,执行中路径的状态,会被保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列。这就是上下文切换

3.为什么长时间的处理不放在主线程执行?

程序在启动时,最先执行的线程是主线程,用来描绘用户界面、处理屏幕触摸的事件。如果主线程进行长时间的处理,机会导致主线程阻塞,妨碍主线程中RunLoop主循环的执行,导致不能更新用户界面、应用画面长时间停滞等问题。

1.GCD的API

1. DIspatch Queue

名词解释

官方对GCD的说明:

开发者要做的只是定义想执行的任务,并追加到适当的Dispatch Queue中。

源码的表示就是:

dispatch_sync(queue, ^{
        //your work here;
    });
[4] → | [3] [2] [1] | → [0]
追加    Dispatch Queue  已处理 

Dispatch Queue遵循FIFO原则,先进先出。

Dispatch Queue的种类

  • Serial Dispatch Queue :串行,一个线程
  • Concurrent Dispatch Queue :并行,多线程

2.如何得到Dispatch Queue

1.通过CGD的API生成

以下代码生成了Serial Dispatch Queue

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

以下代码生成了Concurrent Queue

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.myGCDConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

关于Serial Dispatch Queue的个数

一个Serial Dispatch Queue同时只能追加一个处理。

多个Serial DIspatch Queue各追加处理,并同时执行,就变成并行执行。

注意,一旦生成1个Serial DIspatch Queue,系统就对一个Queue生成并使用一个线程,如果生成2000个,那就生成2000个线程。

因此,只是在为了避免多个线程更新相同资源导致的数据竞争时使用Serial DIspatch Queue

关于dispatch_queue_create方法

dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr)

第一个参数为queue的名称,推荐输入逆序全程域名,崩溃时会出现在log中。
第二个参数为queue的类型,如果输入NULL则为Serial,如果输入DISPATCH_QUEUE_CONCURRENT则为Concurrent。

另:iOS6之后,DIspatch Queue也加入了ARC,如果手动设置Retain和Release,会报错。

将Block内的执行追加到Queue中

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.myGCDConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{
    NSLog(@"v-tech is the BEST!");
});

2.获取系统标准提供的Dispatch Queue

系统提供的Dispatch Queue有:

  • Main Dispatch Queue
  • Global Dispatch Queue

Main Dispatch Queue是在主线程执行的Dispatch Queue,只有一个,是Serial Dispatch Queue。追加到Main Dispatch Queue的处理在主线程的RunLoop中执行,因此必须将用户界面更新等一些必须在主线程中执行的处理追加到Main Dispatch Queue中。

Global Dispatch Queue是所有程序都可以使用的Concurrent Dispatch Queue。没有必要通过dispatch_queue_create逐个生成Concurrent Dispatch Queue,直接获取Global Dispatch Queue就行了。

获取Main Dispatch Queue和 Global Dispatch Queue的方法

//main
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
    
//global
dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(long identifier, unsigned long flags);

//global example
dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

获取Main Dispatch Queue的方法只有一个。

获取Global的方法里有两个变量,第一个是设定优先级,有以下四个可以选择:


第二个参数传入0即可。

文档关于dispatch_get_global_queue的介绍:

/*!
 * @function dispatch_get_global_queue
 *
 * @abstract
 * Returns a well-known global concurrent queue of a given quality of service
 * class.
 *
 * @discussion
 * The well-known global concurrent queues may not be modified. Calls to
 * dispatch_suspend(), dispatch_resume(), dispatch_set_context(), etc., will
 * have no effect when used with queues returned by this function.
 *
 * @param identifier
 * A quality of service class defined in qos_class_t or a priority defined in
 * dispatch_queue_priority_t.
 *
 * It is recommended to use quality of service class values to identify the
 * well-known global concurrent queues:
 *  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
 *
 * The global concurrent queues may still be identified by their priority,
 * which map to the following QOS classes:
 *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
 *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
 *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
 *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
 *
 * @param flags
 * Reserved for future use. Passing any value other than zero may result in
 * a NULL return value.
 *
 * @result
 * Returns the requested global queue or NULL if the requested global queue
 * does not exist.
 */

3.延迟处理

场景:在3秒后将指定的Block追加到Main Dispatch Queue

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);

dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"wait at least 3 second");
    });

注意,代码指的不是3秒后执行Block内的处理,而是在3秒后将执行追加到Queue中执行。

因为Main Dispatch Queue 在主线程的RunLoop中执行,所以在比如每隔1/60秒执行的RunLoop中,Block最快在3秒后执行,最慢在3秒+1/60秒后执行,并且在Main Dispatch Queue有大量处理追加或者主线程的处理本身有延迟,这个时间会更长。

dispatch_time方法解析

dispatch_time(dispatch_time_t when, int64_t delta)

第一个参数是时间点,DISPATCH_TIME_NOW意思是现在。

第二个参数是距离时间点的长度,也就是多少时间之后追加到Queue中,格式如下:

3ull * NSEC_PER_SEC 
/*
* 3ull * NSEC_PER_SEC 表示3秒
* 
* ull = unsigned long long
*
* #define NSEC_PER_SEC 1000000000ull
* #define NSEC_PER_MSEC 1000000ull
* #define USEC_PER_SEC 1000000ull
* #define NSEC_PER_USEC 1000ull
*
*/

4.等待所有任务完成后执行其他处理

1.Dispatch Group

场景:在追加到Dispatch Queue中的多个处理全部结束之后执行其他处理

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
    
dispatch_group_async(group, queue, ^{
        NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
        NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
        NSLog(@"blk2");
});
    
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"other method");
});

dispatch_group_create()生成dispatch_group_t类的Dispatch Group。

dispatch_group_asyncdispatch——async相同,都是追加Block到指定的Dispatch Queue中。不同的是,指定的Block属于指定的Dispatch Group。

在追加到Dispatch Group中的处理全部执行结束后,该源代码中使用的dispatch_group_notify函数会将执行的Block追加到Dispatch Queue中,将第一个参数指定为要监视的Dispatch Group。在追加到改Group的全部处理执行解释后,将第三个参数的Block追加到第二个参数的Dispatch Queue中。在dispatch_group_notify中不管制定什么样的Dispatch Queue,属于Dispatch Group的全部处理在追加指定的Block时已经结束。

dispatch_group_async的介绍

/*!
 * @function dispatch_group_async
 *
 * @abstract
 * Submits a block to a dispatch queue and associates the block with the given
 * dispatch group.
 *
 * @discussion
 * Submits a block to a dispatch queue and associates the block with the given
 * dispatch group. The dispatch group may be used to wait for the completion
 * of the blocks it references.
 *
 * @param group
 * A dispatch group to associate with the submitted block.
 * The result of passing NULL in this parameter is undefined.
 *
 * @param queue
 * The dispatch queue to which the block will be submitted for asynchronous
 * invocation.
 *
 * @param block
 * The block to perform asynchronously.
 */

2.Dispatch Group Wait

在Group中也可以使用dispatch_group_wait函数仅等待全部处理执行结束。

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

forever的意思是永远等待,只要Group里面的处理尚未执行,就会一直等待。

如同dispatch_after一样,制定等待间隔为1秒的处理如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk0");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk2");
    });
    
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
    
    long result = dispatch_group_wait(group, time);
    
    if(result == 0){
        //属于Dispatch Group的处理全部执行结束
        NSLog(@"done");
    }else{
        //属于Dispatch Group的某一个处理还在进行 或者 超时了
        NSLog(@"doing");
    }

其中,dispatch_group_wait(group, time)中的time是time-ou时间,如果到了time制定的时间还没有完成处理,就直接返回非零值,表示超时了。

如果不想等待,直接判定,可以把时间设定为DISPATCH_TIME_NOW。如果要一直等待,就设定为DISPATCH_TIME_FOREVER

3.notifywait

在主线程RunLoop的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间,虽然这样也可以,但一般这种情况下,还是推荐使用notify,函数追加结束处理到Main Dispatch Queue中们可以简化源代码。

5.处理写入处理的数据竞争

写入处理不能并行执行,会引发数据竞争,但是读取处理可以,因此,为了高效访问,读取处理可以追加到Concurrent Dispatch Queue,写入处理可以在任一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中(写入处理结束之前,读取处理不可执行)。

使用dispatch_barrier_async函数

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

推荐阅读更多精彩内容