【iOS】块与大中枢派发

前言

块与大中枢派发

开发应用程序时,应该多留意多线程问题。即使开发的应用程序用不到多线程,他们仍可能是多线程的,因为系统框架通常会在UI线程之外再使用一些线程来执行任务。
当前多线程编程的核心是“块”与“大中枢派发”。块是一种可以在C,C++,及Objective-C代码中使用的“词法闭包”,借此机制,开发者可以将代码像对象一样传递,令其在不同环境下运行。在定义“块”的范围内,它可以访问到其中的全部变量
GCD是一种与块有关的技术,它提供了对线程的抽象,这种抽象基于“派发队列”。开发者可以将块排入队列中,由GCD负责处理所以调度事宜。GCD会根据系统资源情况,适时的创建,复用,摧毁后台线程,以便处理每个队列。GCD还可以方便的完成常见编程任务,比如编写“只执行一次线程安全的代码”或者根据可用的系统之一来兵法之行多个操作。


提示:以下是本篇文章正文内容,下面案例可供参考

理解“块”这一概念

  • “块”可以实现闭包。这项特性是作为”拓展“加入GCC编译器中的,在近期版本中的Clang中都可以使用。这是一个位于C语言层面的特性,因此,只要有支持此特性的编译器,以及能执行块的运行期组件,就可以在C,C++,Objective-C,Objective-C++,代码中使用。

块的基础知识

  • 块与函数类似,块是直接定义在另一个函数里表示的。和定义它的那个函数共享同一个范围内的东西。块用符号“^”来表示。后面跟着一对花括号,括号里是块的实现代码。
  • 块其实就是一个值,而且有其相关类型。与int,float,或Objectiove-C对象一样,也可以把这个歌块付给变量,然后像使用其他变量那样使用它。块类型的语法与函数类。
void (^someBlock) () = ^{
    
};
  • 定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去有点怪。
return_type (^block_name)(parameters)


int (^addBlock)(int a, int b) = ^(int a, int b){
    return a + b;
};


int add = addBlock(2,5);
//add = 7;
  • 块的强大之处在于:声明他的范围里,所以变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里仍然可以使用。默认情况下,为快所捕获的变量,是不可以在块里修改的。如果声明变量的时候加上_bolk修饰符,就可以在块内修改了。
  • 如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。块本身可以视作对象,实际上在其他OC所能响应的选择子中途,有很多块也可以响应。最重要的是:块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量。以便平衡捕获时所执行的保留操作。
  • 如果将块定义在Objective-C类的实例丰富中,那么除了可以访问类的所以实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无需加_block。如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获。因为实例变量时与self所指代的实例关联在一起的。
  • self也是一个对象,块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况通常会导致“保留环”。

块的内部结构

  • 每个Objective-C对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象所占内存区域也有大有小。块本身是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,叫做isa。其余内存里含有块对象政策运转所需的各种信息。请添加图片描述
  • 在内存布局中,最重要的是invoke变量。这是一个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。块其实就是一种代替函数指针的语法结构,原来使用函数指针时,需要用不透明的void指针来传递状态。改用块之后,可以吧原来用标准的C语言特性所编的代码封装成简明且易用的接口。
  • descriptor变量指向结构体的指针,每个块里都包含此结构体。其中声明了块对象总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,钱折腰保留捕获的对象,后者将之释放。
  • 块还会把他锁捕获的所有变量都拷贝一份。这些拷贝对象放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。(拷贝的是 指向这些对象的指针变量)。因为执行块时,要把变量从内存中读出来,invoke函数需要把块作为对象传进来。

全局块,栈块,堆块

  • 定义块的时候,所占内存区域是分配在栈中的。块只在定义他的那个范围内有效。
  • 可以给块对象发送copy消息拷贝来解决这个问题。这样的话,就可以把块从栈复制到堆。拷贝后的块,可以定义在他的那个范围之外使用。一旦复制到堆上,块就变成了带引用计数的对象了。后续的复制操作都不会真的复制,只是递增块对象的引用计数。如果不在使用这个块,应该将其释放。在ARC环境下会自动释放,手动管理引用计数需要自己来调用release方法。当引用计数降为0后,“分配在堆上的块”会像其他对象一样,为系统所回收。“分配在栈上的块”无需明确释放。因为栈内存本来就会自动回收。
  • 全局块不会不做任何状态,运行时也无需有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了。因此,全局块可以声明在全局内存里,而不需要在每次用到的时候与栈中创建。全局块的拷贝相当于一个空操作,因为全局块绝不可能为系统回收。相当于一个单例。

为常用的块类型创建typedef

  • 每个块都有用其固有类型,因而可以将其复制给适当类型的变量。这个类型由块所接受的参数机器返回值组成。
  • 与其它类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。可以起一个更为易读的名字来表示块,把块的类型隐藏在后面。
typedef int(^EOCSomeBlock) (BOOL flag, int value);
  • typedef关键字用于给类型起一个易读的名。
  • 声明变量时,要把名称放在类型中间,并在前面加上^符号,而定义新类型时也得这么做。上面的语句向系统中新增了一个名为EOCSomeBlock的类型,此后,直接使用新类型即可。
  • 与定义的其他变量一样,变量类型在左边,变量名在右边。 通过这个特性,可以把使用块的API做的更容易一些。类里面有些方法可能需要用块来做参数,比如执行异步任务时所用的completion handler。参数就是块,但凡遇到这种情况,都可以通过定义别名使代码更为易读。
  • 定义方法参数所用的块类型语法,又和定义变量时不同。若能把方法签名中的参数类型写成一个词,那么读起来就顺口多了。于是,可以给参数类型七个别名,然后使用此名称来定义。
  • 使用类型定义还有一个好处,就是当你打算重构块的类型签名时会很方便。
  • 最后在使用块类型的类中定义这些typedef,而且应该把这个类的名字加在由typedef所定义的新类型的后面。这样可以阐明块的用途。还可以用typedef给同一个块签名类型创建数个别名。
  • 如果由好几个类都要执行相似但各有区别的异步任务,而这几个类又不能防乳同一个体系继承,每个类就应该有自己的completion handler类型。这个几个completion handler的签名签名也许完全相同,但最好还是在每个类里都各自定义一个别名,而不要共用一个名称。繁殖,若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,供子类使用。

用handler块降低代码分散程度

  • 为用户界面编码时,常用范式“异步执行任务”。这种范式的有点在于:处理用户界面的现实及触摸操作所用的线程,不会因为要执行I/O或网络通信这类好事的任务而阻塞。这个线程通常称为主线程。如果把执行异步任务做成同步的,那么在执行任务时,用户界面就变得无法响应用户输入了。
  • 异步方法在执行完任务后,需要以某种手段通知相关代码。比如通过委托协议,令关注此事件的对象遵循该协议。对象成为delegate后,就可以在相关事件发生时得到通知。
  • 如果用块来写一个从URL中获取数据的类,嗲吗会更加氢气。块可以令这种API变得更为紧致,同时令开发者调用起来更方便。把completion handler定义为块类型,将其当作参数传递给方法。这种模式和委托协议很像,但是可以在调用方法时直接以内联形式定义completion handler,以此方式来使用“网络数据获取器”,可以令代码比原先易读
  • 与委托模式的代码相比。块写出来的代码更简洁。异步任务执行完毕后所需运行的业务逻辑,和启动一步任务所应的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。
  • 委托模式有一个缺点,如果类要分别使用多个获取器下载不同数据,那么就得在delegate回调方法里根据传入的获取器参数切换。这样做会令delegate回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量。用块来改写的好处是:无需保存选择器,也无需在回调方法里切换。每个completion handler的业务逻辑,都是和相关的获取器对象一起来定义的。
  • 可以分别用两个处理程序来处理操作失败和成功的不同情况。也可以吧处理失败的情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里。由于成功和失败的情况分开处理,所以调用此API的代码也会按照逻辑,吧应对成功和失败的代码分开来写,令代码可读性更高。
  • 如果把全部逻辑写在一起,会令块变得比较长,比较复杂。都是使用一个块来处理也更为灵活。调用API的代码也可能会在处理成功响应的过程中发现错误。如果把成功和失败情况交给老公不同的处理程序来负责,那么就没办法共享同一份错误处理代码里,除非吧这段代码单独放在一个方法里,但是这样违背了我们想把全部逻辑代码放在一处的初衷。
  • 有时候在相关事件点执行回调操作,这种情况也可以使用handler块。
  • 基于handler来设计API还有一个原因,某些代码必须运行在特定的线程上。因此,最好能由调用API的人来决定handler应该运行在哪个线程上。NSNotificationCenter就是这种APi,它提供了一个方法, 调用者可以经由此方法来注册想要接收的通知等到相关事件发生时,通知中心就会执行注册号的那个块。调用者可以指定某个块应该安排在哪个执行队列里。若没有指定队列,按照默认方法执行,也就是由投递通知的那个线程来执行。

用块引用其所属对象时不要出现保留环

  • 使用块时,若不仔细思量,很容易导致“保留环”。
  • 如果设计API时用到了completion handler这样的回调块,那么很容易形成保留环。一般来说,只要适时清理掉环中的某个引用,即可解决此问题。
  • 如果completion handler块所引用的对象最终又引用了这个块本身,那么就会出现保留环。
  • 如果块所捕获的对象直接或间接保留了块本身,那么就得当心保留环
  • 一定要找一个适当的时机解除保留环,而不能把责任推卸给API的调用者。

多用派发系列,少用同步锁

  • 在Objective-C中,如果有多个线程要执行同一份代码,那么又是可能会出现问题。这个时候通常使用锁实现同步机制。在GCD出现前,有两种方法。
  • 第一种是采用内置的“同步块”。这种写法会根据给定对象,自动创建一个锁,并等待块中的代码执行完毕,执行到代码结束时,锁就释放了。
  • 另一种方法是直接使用NSLock对象。也可以使用NSRecursiveLock这种“递归锁”,线程能够多次持有该锁,而不会出现死锁现象。
  • 在极端情况下,同步锁会导致死锁,效率也不一定提高,如果直接使用锁对象,一旦遇到死锁,就会非常麻烦。
  • 代替方案就是使用GCD,能更简单,更高效的为代码加锁。
  • 使用“串行同步队列”,将读取操作都安排在同一个队列里,即可保证数据同步。这种方法可以更简单,高效的代替同步块或锁对象。
  • 这种模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作就都同步了。为了使块代码能够设置局部变量,获取方法中用到了__block语法。若是抛开这一点,这种写法比签名那些更简洁。全部加锁任务都在GCD中处理,而GCD是在相当深的底层来实现的。于是能够做许多优化。
  • 还可以进行一步优化,设置方法不一定非得是同步的,设置实例变量所用的块,并不需要向设置方法返回什么值。
  • 把同步派发改为异步派发时,从调用者的角度看,这个小改动可以提升设置方法的执行速度,而读取操作与写入操作依然会按照顺序执行。但是也有一个坏处:这种写法比原来慢。因为执行异步派发时,需要拷贝块。若拷贝块的时间明显超过执行块的时间,真宗方法会比原来慢。
  • 多个获取方法可以并发执行,而获取次方法与设置方法之间不能并发执行,利用这个特点,可以写出来更快的代码。
  • 在队列中,栅栏块必须单独进行,不能与其他块并行。这只是对并发队列有意义,因为串行队列中的块总是按照顺序来逐个执行。并发队列如果发现接下来的要处理的块是栅栏块,那么就一直要等到当前所有的并发块都执行完毕,才会单独执行这个歌栅栏块。栅栏块执行完毕后,在按照正常方式继续向下执行。请添加图片描述
  • 这种做法肯定会比使用串行队列要快。注意,设置函数也可以改用同步的栅栏块来实现,这样可能更高细哦啊。最好还是测一测每种方式的性能,选择最好的。

GCD

GCD是什么?

  • GCD(Grand Cenreal Dispatch)是异步执行任务的技术之一。一般将应用程序中记述的线程管理中的代码在系统中实现。开发者只需要定义想执行的任务并追加到适合的Dispatch Queue中,GCD就能生存必要的线程并计划执行任务。

多线程编程

  • 线程:“1个CPU执行的CPU命令列为1条无分岔路径”。
  • 一个物理CPU芯片实际上有64个(64核)CPU,如果1个CPU核虚拟为两个CPU核工作,那么一台计算机上使用多个CPU核就是理所当然的事情了。尽管如此,“1个CPU核执行的CPU命令列为一条无分叉路径”仍然不变。
  • 这条无分叉路径不止一条,存在多个时即为“多线程”。在多线程中,1个CPU核执行多条不同路径上的不同命令。(多条路径–多线程)
  • iOS和OS X的核心XUN内核杂气发生操作系统事件时(如每隔一段时间,唤起系统调用等情况)会切换执行路径,执行路径中的状态,如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU的寄存器等信息,继续执行切换路径的CPU命令列,这些被称为“上下文切换”。
  • 使用多线程的程序可以在某个线程之间反复横条进行上下文切换,看上去就好像1个CPU核能够并列地执行多个线程一样。而且在具有多个CPU核的情况下,就不是“看上去像”,是真的提供了多个CPU核并行多个线程的技术。这种利用多线程编程的技术被称为“多线程编程”。
优点缺点
能保证程序的响应性能多个线程更新相同的资源会导致数据的不一致(数据竞争)。停止等待事件的线程会导致多个线程相互持续等待(死锁)。大量消耗内存。
  • 程序在启动时,通过最先执行的线程,即“主线程”来描绘用户界面,处理屏幕的事件等。如果在该主线程中进行长时间的处理,会妨碍主线程的执行。

GCD的API

  • Dispatch Queue

“Dispatch Queue”是执行处理的等待队列。应用程序编程人员通过dispatch_async等函数API,在Block语法中记述想要执行的处理并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序,执行处理。

执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的 Concurrent Dispatch Queue。

Serial Dispatch QueueConcurrent Dispatch Queue
等待现在执行中处理结束不等待现在执行中处理结束
顺序执行并行执行
  • dispatch_queue_create

第一种方法是通过GCD的API生成 Dispatch Queue。dispatch_queue_create函数可生成Dispatch Queue。

  • Main Dispatch Queue/Global Dispatch Queue

第二种方法是获取系统标准提供的Dispatch Queue。
Main Dispatch Queue/Global Dispatch Queue是不特意生成的系统提供的Dispatch Queue。

Main Dispatch QueueGlobal Dispatch Queue
在主线程中执行的Dispatch Queue所有应用程序都能使用
Serial Dispatch QueueConcurrent Dispatch Queue
  • dispatch_set_target_queue

dispatch_set_target_queue函数生成的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue都默认和优先级Global Dispatch Queue相同执行优先的线程。

  • dispatch_after

想在指定时间后执行处理的情况,可以使用dispatch_after。
dispatch_after不是在指定时间后执行处理,而是在指定时间追加处理到Dispatch Queue。和在指定时间后使用dispatch_async函数追加Block到Main Dispatch Queue的相同。

  • Dispatch Group

在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理,这种情况经常出现。只使用一个Serial Dispatch Queue时,只要将想执行的处理全部追加到该Serial Dispatch Queue中并在最后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue时或同时使用多个Dispatch Queue时,情况会很复杂。这种时候使用Dispatch Group。
无论向什么样的Dispatch Queue中追加处理,使用 Dispatch Group都可以监视这些处理执行的结束。

  • dispatch_barrier_async

使用Serial Dispatch Queue可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。如果读取处理只和读取处理并行执行,就不会发生问题。
为了高效率的进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在人一个读取处理没有执行的情况下,追加到Serial Dispatch Queue中即可。
利用Dispatch Group和dispatch_set_target_queue函数也可实现,但会很复杂。可以用dispatch_barrier_async函数同dispatch_queue_create函数生成的Concurrent Dispatch Queue一起使用。

  • dispatch_sync

dispatch_async函数的“async”意味着“非同步”,就是将Block“非同步”地追加到指定的Dispatch Queue中,dispatch_async函数不做任何等待。
dispatch_sync以为着“同步”,也就是将指定的Block“同步”追加到指定的Dispatch Queue中,在追加Block结束之前,dispatch_sync会一直等待。

  • dispatch_apply

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

  • dispatch_suspend/dispatch_resume

当追加大量的处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。这种情况下,挂起Dispatch Queue即可,可以执行时在恢复它。
dispatch_suspend函数挂起指定的Dispatch Queue。
dispatch_resume函数恢复指定的Dispatch Queue。

  • Dispatch Semaphore

Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。

  • dispatch_once

dispatch_once是保证应用程序中只执行一次指定处理的API。(单例模式)

  • Dispatch I/O

在读取较大文件时,如果将文件分成合适的大小并使用Global Dispatch Queue并列读取的话,会比一般的读取快很多。实现这一功能的就是 Dispatch I/O和Dispatch Data。
通过Dispatch I/O读写文件时,使用Global Dispatch Queue将一个文件按某个大小read/write,将文件分为一块一块地进行读取处理。分割的数据通过使用Dispatch Data可以更为简单的结合和分割。

多用GCD,少用performSelector方法

  • Objective-C是一门非常动态的语言。NSObject定义了几个方法,可以随意调用。在出现了大中枢派发及块这样的新技术之后, 这几个方法就没有那么重要了。
 - (id)performSelector:(SEL)selector;
  • 与直接调用选择子等效
[object performSelector:@Selector(selectorName)];
[object selectorName];
  • 这两行代码效果相同。

  • 如果某个方法只是这么来调用,此方法就有一点多余。如果选择子是在运行期决定的,就等于在动态绑定上再次使用动态绑定。
    在这里插入图片描述

  • 这种编程方式很灵活,经常可以用来简化复杂代码。

  • 还有一种用法,把选择子保存起来,等到某个事件发生之后在调用。不论哪个用法,编译器都不知道要执行的选择子是什么。必须等到运行期才能确定。但是在ARC环境下编译代码的话,编译器会发出警告。请添加图片描述
    提示出现内存泄漏。因为编译器不知道将要调用的选择子是什么,也不了解方法签名和返回值,甚至不知道是不是有返回值。由于编译器不知道方法签名,也就没办法运用ARC的内存管理规则来判定返回值是不是应该释放。于是ARC不添加释放操作,这样做就可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

  • 选择子最多只能接收两个参数,也就是调用“performSelector:withObject :withObject:”这个版本。在参数不止两个的时候,没有对应的performSelector方法能够执行此种选择子。

  • performSelector还可以延后执行选择子,或者将其放在另一个线程上进行。

  • 这些方法太过局限,具备延后执行的那些方法无法处理带有两个参数的选择子。能够指定执行线程的方法也不是很通用。

  • 如果改用其他方法,就没有这些限制。最常见的是改用块。performSelector系列方法提供的线程功能,都可以在大中枢派发机制中使用块来实现。延后执行可以使用dsidpatch_after来实现,在另一个线程上可以用dispatch_sync和dispatch_async来实现。

掌握GCD及操作对列的使用时机

  • 要了解每项技巧的使用时机,如果选错了工具,那么编出来的代码就会难以维护。
  • 对于那些只执行一次的代码来说,GCD的dispatch_once最为方便。在执行后台任务时,GCD不一定是最佳方式。还有一种技术叫做NSOperationQueue,开发者可以把操作以NSOperation的子类的形式放在队列中,这些操作也能并发执行。“操作队列”在GCD之前就有了,其中某些设计原理因操作队列流行,GCD就是基于这些原理构造的。
  • GCD是纯C的API,操作队列是Objective-C的对象。在GCD种,任务用块来表示,块是一个轻量级的数据结构。与之相反,操作是一个更重量级的Objective-C对象。但是GCD并不是总是最佳方案,有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而超过其缺点
  • 用NSOperationQueue类的“addOperationQueueWithBlock:”方法搭配NSBlockOperation类来使用操作队列,语法和纯GCD类似。使用NSOperation和NSOperationQueue的好处如下:
    一:取消某个操作:如果使用操作队列,取消操作很容易。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需要执行。已经启动的任务无法取消。若不使用操作队列,把块安排到GCD队列,就无法取消了。那套架构是“安排好任务之后就不管了”。开发者可以在应用程序层之间实现取消功能。这样做需要编写很多代码。
    二:指定操作间的依赖关系。一个操作可以包含其他多个操作。开发者能够指定操作之间的依赖关系,使特定的操作必须在另外一个操作顺利执行完毕后执行。
    三:通过键值观测机制监控NSOperation对象的属性。NSOperation对象有多个属性都适合使用KVO来监听。
    四:指定操作的优先级。操作的优先级便是此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的操作后执行。操作队列的调度方法虽不透明,但是必须经过一番深思熟虑在写成。GCD没有直接实现此功能的方法。但是GCD的队列确实有优先级。那是针对整个队列来说的,而不是针对每个块。NSOperation对象也有线程优先级,这决定了运行此操作的线程处在何种优先级上。用GCD也可以实现此功能,但是采用队列更简单,只要设置一个属性即可。
    五:重用NSOperation对象。系统内设置了一些NSOperation对象的子类供开发者使用。这些子类就是普通的Objective-C对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。
  • 操作队列有很多地方都胜过派发队列。操作队列提供了多种执行任务的方法,都是写好的,直接就能用。开发者不在编写复杂的调度器,也不用自己来实现取消操作或指定操作的优先级的功能。
  • 有一个API选用了操作队列而非派发队列,NSNotificationCenter。开发者可以注册监听器,以便在发生相关事件时得到通知。这个方法接收的参数类型是块,不是选择子。

通过Dispatch Group机制,根据系统资源状况执行任务

  • dispatch group是GCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会接到通知。这个功能有许多用途,最重要的是把将要并发执行的多个任务合为一组,调用者就可以知道这些任务何时能全部执行完毕。
//创建dispatch group
dispatch_group_tdispatch_group_create();
  • dispatch group就是一个简单的数据结构,这种结构彼此之间没有什么区别,它不像派发队列,后者还有一个区别身份的标识符。想把数组编组,有两种方法。第一种是利用函数。请添加图片描述
    他是普通dispatch_async函数的变体,比原来多一个参数,用于表示带执行的块所属的组。
    请添加图片描述
    前者能够使分组里正要执行的任务数递增,后者使其递减。调用前者之后,必须调用后者。和引用计数类似。请添加图片描述
    此函数接收两个参数,一个是要等待的group,另一个是代表等待时间的timeout。timeout表示函数在等待dispatch group执行完毕时,应该阻塞多久。如果执行dispatch group所用时间小于timeout,返回0,否则返回非0值。请添加图片描述
    也可以使用这个函数等待dispatch group执行完毕。开发者可以向此函数传入块,等dispatch group执行完毕后,块会在特定的线程上执行。假如当前线程不应阻塞,开发者又想在那些任务全部完成时得到通知,此做法就很有必要。
  • 一系列任务可归入一个dispatch group之中,开发者可以在这组任务执行完毕时获得通知。
  • 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况调度这些并发执行的任务。

使用dispatch_once来执行只需运行一次的线程安全代码

  • 单例模式的常见实现模式为:在类中编写方法,该方法只会返回全类公用的单例实例,不会每次调用时都创建出新的实例。
    请添加图片描述

  • 为保证线程安全,上述代码将创建单例实例的代码包裹在同步块里。请添加图片描述

  • GCD使用这个函数让实现单例模式更容易。此函数接收类型为dispatch_once_t的特殊参数,还接收块参数。对于给定的标记来说,该函数保证相关的块必定执行,且仅执行一次。首次调用这个函数时,必然执行其中的代码,最重要的是这个操作线程完全安全。对于只执行一次的函数来说,每次调用函数时传入的标记都必须完全相同。
    在这里插入图片描述

  • dispatch_once可以简化代码并彻底保证线程安全,无需担心加锁或同步。所有问题都由GCD在底层处理。每次调用时都必须使用完全相同的标记,所以标记要声明成static。把该变量定义在static作用域中,保证编译器每次执行sharedInstance方法时都会复用这个变量,不会创建新的变量。

  • dispatch_once更高效。他没有使用总量级的同步机制。采用“原子访问”来查询标记,来判断对应代码是否已经执行。

不要使用dispatch_get_current_queue

  • 使用GCD时,经常要判断当前的代码正在哪个队列上执行,向多个队列派发任务时也如此。
dispatch_queue_t dispatch_get_current_queue()
  • 此函数会返回当前正在执行代码的队列。但是我们最好还是不要使用,因为队列之间会形成一套层级体系,这意味着在某个队列中的块,会在其上级队列(“父队列”)里执行。层级里地位最高的那个队列总是“全局并发队列”。这样我们再进行检测的时候,他返回的其实并不是我们想要队列,而可能是其子队列,若对其进行条件的判断就有可能造成“死锁”。
    在这里插入图片描述

请添加图片描述

  • 要解决这个问题,最好的办法就是通过GCD所提供的功能来设定“队列特有数据”,此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定键获取不到关联数据,那么系统就会沿着层级向上查找,知道找到数据或到达根队列为止。
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);
  • 此函数的首个参数表示待设置数据的队列,其后两个参数是键与值。键与值都是不透明的void指针。对于键来说,有个问题一定要注意:函数是按指针值来比较键的,而不是按照其内容。函数最后一个参数是“析构函数”,对于给定的键来说,当队列所占内存为系所回收,或者有新的值与键相关联时,原有的值对象就会移除,而析构函数也会于此时运行。dispatch_function_t类型的定义如下:
typedef void (*dispatch_function_t)(void*);
  • dispatch_get_current_queue函数的行为常常和开发者预期的不同,此函数已经被废弃。只应该做调试之用。
  • 派发队列是按层级来阻止的,无法单独使用某个队列对象来描述“当前队列”这一概念。
  • dispatch_get_current_queue函数用于解决由不可重入的代码所引发的思索,但是能用此函数解决的问题,也能用“队列特定数据来解决”
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山河丘壑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值