iOS多线程开发——GCD的使用与多线程开发浅析(二)

时间:2022-05-06 16:17:35

        对于iOS多线程开发,我们时刻处于学习之中,在看书中,看文档中,项目开发中,都可以去提高自己。最近刚看完了《Objective-C高级编程 iOS与OS X多线程和内存管理》这本书后,对多线程有了更为深入的理解,故在此做一个总结与记录。这本书是iOS开发者必读的书之一,写得很不错。书的封面如下,故也称狮子书:

iOS多线程开发——GCD的使用与多线程开发浅析(二)

(1)多线程会遇到的问题

iOS多线程开发——GCD的使用与多线程开发浅析(二)

多线程会出现什么问题呢?当多个线程对同一个数据进行修改的时候,会造成数据不一致的情况;当多个线程互相等待会造成死锁;过多的线程并发会大量消耗内存。所以多线程出现bug会严重影响App的功能和性能。


(2)多线程的作用

我们为什么一定要用多线程呢?只要有一个主线程不就可以了么。

iOS多线程开发——GCD的使用与多线程开发浅析(二)


从图中可以看到,如果我们有一个很耗时的操作放到主线程中执行,那么会严重阻塞主线程的执行,导致主界面不能及时响应用户的操作,出现卡死状态。当创建多线程之后,我们可以把耗时操作放到其他线程中去执行,比如网络操作,图片的上传下载等,当执行成功后再回到主线程更新界面即可。所以,使用多线程是必要的。


(3)GCD中的队列——Dispatch Queue

iOS多线程开发——GCD的使用与多线程开发浅析(二)

Dispatch Queue就是GCD中的调度队列,我们只要把任务添加到队列中去,线程就会按照顺序取出然后去执行,按照先进先出FIFO的原则。



(4)Dispatch Queue的种类

GCD中存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue(串行队列);另一种是不等待现在执行中处理的Concurrent Dispatch Queue(并发队列).

Serial Dispatch Queue:等待现在执行中处理结束;

Concurrent Dispatch Queue:不等待现在执行中处理结束,使用多个线程同时执行多个处理。

iOS多线程开发——GCD的使用与多线程开发浅析(二)


Serial Dispatch Queue为什么一定要等待处理结束才去执行下一个处理呢?因为Serial Dispatch Queue只有一个线程,一个线程同一时间当然只能执行一个任务,所以后续任务必须要进行等待。

Concurrent Dispatch Queue由于可以创建多个线程,所以只要按顺序取出任务即可,把取出的任务放到不同的线程去执行,任务之间是否执行结束没有必然关系,所以不需要等待前一个处理结束,看起来就像是多个任务同时在执行一样,其实也的确是在同时执行。当然这个并发数量系统会有限制,我们代码也可以设置最大并发数。

看了以上的解释,就知道Serial Dispatch Queue、Concurrent Dispatch Queue和线程的关系了,关系如下:

iOS多线程开发——GCD的使用与多线程开发浅析(二)



(5)多个Serial Dispatch Queue实现并发,以及遇到的问题

      当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue将并行执行。虽然在一个Serial Dispatch Queue中同时只能执行一个追加处理,但是如果将处理分别追加到4个Serial Dispatch Queue中,各个Serial Dispatch Queue执行一个,即可以同时执行四个处理。

iOS多线程开发——GCD的使用与多线程开发浅析(二)

虽然这种笨办法也可以实现并发,但是也会遇到大问题,那就是消耗大量内存:

iOS多线程开发——GCD的使用与多线程开发浅析(二)



(6)资源竞争问题的解决

当多个线程对同一数据进行操作时可造成竞争或者数据不一致。最简单的解决办法就是使用Serial Dispatch Queue。Serial Dispatch Queue只创建一个线程,而一次只能执行一个任务,只有当该任务执行结束才能去执行下一个,所以同一时间对某个竞争资源的访问是唯一的。示意图如下:

iOS多线程开发——GCD的使用与多线程开发浅析(二)


(7)生成的Dispatch Queue必须由程序员释放。这是因为Dispatch Queue并没有像Block那样具有作为OC对象来处理的技术。通过dispatch_queue_create函数生成的Dispatch Queue在使用结束后通过dispatch_release释放。看个下面的例子:

iOS多线程开发——GCD的使用与多线程开发浅析(二)

这样立即释放queue是否有问题呢?

在dispatch_async函数中追加Block到Dispatch Queue后,即使立即释放Dispatch Queue,该Dispatch Queue由于被Block持有也不会被废弃,因而Block能够执行。Block执行结束后会释放Dispatch Queue,这时谁都不持有Dispatch Queue,因此它会被废弃。


(8)系统标准提供的Dispatch Queue

-- Main Dispatch Queue:在主线程中执行的queue,因为主线程只有一个,所以Main Dispatch Queue自然就是Serial Dispatch Queue。追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。

iOS多线程开发——GCD的使用与多线程开发浅析(二)



-- Global Dispatch Queue:是所有应用程序都能够使用的Concurrent Dispatch Queue,没有必要通过dispatch_queue_create函数逐个生成Concurrent Dispatch Queue,只要获取Global Dispatch Queue即可。其中有四个优先级,但是用于Global Dispatch Queue的线程并不能保证实时性,因此执行优先级只是大致的判断。

对于Main Dispatch Queue和Global Dispatch Queue执行dispatch_retain函数和dispatch_release函数不会引起任何变化,也不会有任何问题。这也是获取并使用Global Dispatch Queue比生成、使用、释放Concurrent Dispatch Queue更轻松的原因。


(9)dispatch_set_target_queue:改变生成的Dispatch Queue的执行优先级

指定要变更执行优先级的Dispatch Queue为dispatch_set_target_queue函数的第一个参数,指定与要使用的执行优先级相同的Global Dispatch Queue为第二个参数(目标),第一个参数如果指定系统提供的Main Dispatch Queue和Global Dispatch Queue,则不会知道出现什么状态,因此这些均不可指定。


(10)dispatch_after:延迟执行

NSEC_PER_SEC:秒

NSEC_PER_MSEC:毫秒


(11)dispatch_barrier_async

会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕后,Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始并行执行。示意图如下:

iOS多线程开发——GCD的使用与多线程开发浅析(二)..


(12)dispatch_async

将指定的block“非同步”的追加到指定的Dispatch Queue中,dispatch_async函数不做任何等待。

iOS多线程开发——GCD的使用与多线程开发浅析(二)



(13)dispatch_sync造成的问题

一旦调用dispatch_sync函数,那么在指定的处理执行结束之前,该函数不会返回。但是dispatch_sync容易造成死锁。

iOS多线程开发——GCD的使用与多线程开发浅析(二)

该源代码在Main Dispatch Queue即主线程中执行指定的Block,并等待其执行结束。而其实在主线程中正在执行这些源代码,所以无法执行追加到Main Dispatch Queue的Block。下面的例子也一样:

iOS多线程开发——GCD的使用与多线程开发浅析(二)

Main Dispatch Queue中执行的Block等待Main Dispatch Queue中要执行的Block执行结束。当然Serial Dispatch Queue也会引起相同的问题。

iOS多线程开发——GCD的使用与多线程开发浅析(二)


(14)dispatch_apply

dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API。该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部处理结束。

iOS多线程开发——GCD的使用与多线程开发浅析(二)


因为在Global Dispatch Queue中执行处理,所以各个处理的执行时间不定,是不进行等待的追加任务。但是输出结果中最后的done必定在最后的位置上。这是因为dispatch_apply函数会等待全部处理执行结束。

iOS多线程开发——GCD的使用与多线程开发浅析(二)

由于dispatch_apply函数也与dispatch_sync函数相同,会等待处理执行结束,因此推荐在dispatch_async函数中非同步的执行dispatch_apply函数。


iOS多线程开发——GCD的使用与多线程开发浅析(二)


(15)dispatch_suspend/dispatch_resume

当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。

-- dispatch_suspend函数挂起指定的Dispatch Queue:

dispatch_suspend(queue);


dispatch_resume函数恢复指定的Dispatch Queue:

dispatch_resume(queue);


这些函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。


(16)Dispatch Semaphore

当不使用信号量的时候出现的bug.

iOS多线程开发——GCD的使用与多线程开发浅析(二).

这里使用Global Dispatch Queue更新NSMutableArray类对象,所以执行后由内存错误导致应用程序异常结束的概率很高。

Dispatch Semaphore是持有计数信号,该计数是多线程编程中的计数类型信号。计数为0时等待,计数为1或者大于1时,减去1而不等待。

创建semaphore:

iOS多线程开发——GCD的使用与多线程开发浅析(二)

参数表示计数的初始值。

iOS多线程开发——GCD的使用与多线程开发浅析(二)

dispatch_semaphore_wait函数等待Dispatch Semaphore的计数值达到或者等于1.当计数值大于等于1,或者在等待中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait函数返回。

semaphore可以进行以下分支处理:

iOS多线程开发——GCD的使用与多线程开发浅析(二)


dispatch_semaphore_wait函数返回0时,可安全的执行需要进行排他控制的处理。该处理结束时通过dispatch_semaphore_signal函数将Dispatch Semaphore的计数数值加1.

案例:

iOS多线程开发——GCD的使用与多线程开发浅析(二)

iOS多线程开发——GCD的使用与多线程开发浅析(二)



(17)dispatch_once

dispatch_once函数是保证在应用程序执行中只执行一次指定处理的API。下面这种经常出现的用来初始化的源代码可通过dispatch_once函数简化:
iOS多线程开发——GCD的使用与多线程开发浅析(二)


在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。而用dispatch_once函数初始化就不用担心了。这就是单例模式,在生成单例对象时使用。


(18)GCD的基本实现与描述

苹果官方说明:通常,应用程序中编写的线程管理用的代码要在系统级实现。

什么是系统级实现?就是在iOS和macOS的核心XNU内核级上实现。因此,无论程序员如何努力编写管理线程的代码,在性能方面也不可能胜过XNU内核级所实现的GCD。

用于实现Dispatch Queue而使用的软件组件:

iOS多线程开发——GCD的使用与多线程开发浅析(二)

Dispatch Queue没有“取消”这一概念。一旦将处理追加到Dispatch Queue中,就没有办法可以将该处理删除,也没有办法就执行中取消该处理。