Block的复制时机以及几种类型

什么时候栈上的Block会被复制到堆呢?
1. 调用block的copy函数时;
2. Block作为函数返回值返回时;
3. 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时;
4. 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时;
在 ARC 中,捕获外部了变量的 block 的类会是 NSMallocBlock 或者 NSStackBlock,如果 block 被赋值给了某个变量在这个过程中会执行 Blockcopy 将原有的 NSStackBlock 变成 NSMallocBlock;但是如果 block 没有被赋值给某个变量,那它的类型就是 NSStackBlock;没有捕获外部变量的 block 的类会是 NSGlobalBlock 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。
在非 ARC 中,捕获了外部变量的 block 的类会是 NSStackBlock,放置在栈上,没有捕获外部变量的 block 时与 ARC 环境下情况相同。

block中的isa指向的是该block的Class。在block runtime中,定义了6种类:
_NSConcreteStackBlock 栈上创建的block
_NSConcreteMallocBlock 堆上创建的block
_NSConcreteGlobalBlock 作为全局变量的block
_NSConcreteWeakBlockVariable
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock

其中我们能接触到的主要是前3种,后三种用于GC不再讨论..
当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。

当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。

2018/4/12 posted in  GCD

GCD使用总结

GCD是iOS的一种底层多线程机制,今天总结一下GCD的常用API和概念,希望对大家的学习起到帮助作用。

GCD队列的概念

在多线程开发当中,程序员只要将想做的事情定义好,并追加到DispatchQueue(派发队列)当中就好了。
派发队列分为两种,一种是串行队列(SerialDispatchQueue),一种是并行队列(ConcurrentDispatchQueue)。

一个任务就是一个block,比如,将任务添加到队列中的代码是:

dispatch_async(queue, block);

当给queue添加多个任务时,如果queue是串行队列,则它们按顺序一个个执行,同时处理的任务只有一个。
当queue是并行队列时,不论第一个任务是否结束,都会立刻开始执行后面的任务,也就是可以同时执行多个任务。
但是并行执行的任务数量取决于XNU内核,是不可控的。比如,如果同时执行10个任务,那么10个任务并不是开启10个线程,线程会根据任务执行情况复用,由系统控制。

获取队列

系统提供了两个队列,一个是MainDispatchQueue,一个是GlobalDispatchQueue
前者会将任务插入主线程的RunLoop当中去执行,所以显然是个串行队列,我们可以使用它来更新UI。
后者则是一个全局的并行队列,有高、默认、低和后台4个优先级。

它们的获取方式如下:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRORITY_DEFAULT, 0)

执行异步任务

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);     dispatch_async(queue, ^{ 
    //...
});

这个代码片段直接在子线程里执行了一个任务块。使用GCD方式任务是立即开始执行的
它不像操作队列那样可以手动启动,同样,缺点也是它的不可控性。

令任务只执行一次

+ (id)shareInstance {      
+ static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         _shareInstance = [[self alloc] init];
     }); 
 }

这种只执行一次且线程安全的方式经常出现在单例构造器当中。

任务组

有时候,我们希望多个任务同时(在多个线程里)执行,再他们都完成之后,再执行其他的任务,
于是可以建立一个分组,让多个任务形成一个组,下面的代码在组中多个任务都执行完毕之后再执行后续的任务:

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(@"1"); });  
dispatch_group_async(group, queue, ^{ NSLog(@"2"); }); 
dispatch_group_async(group, queue, ^{ NSLog(@"3"); }); 
dispatch_group_async(group, queue, ^{ NSLog(@"4"); }); 
dispatch_group_async(group, queue, ^{ NSLog(@"5"); }); 

dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 
    NSLog(@"done"); 
});

延迟执行任务

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 
    //... 
});

这段代码将会在10秒后将任务插入RunLoop当中。

dispatch_asycndispatch_sync

先前已经有过一个使用dispatch_async执行异步任务的一个例子,下面来看一段代码:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);     
dispatch_async(queue, ^{ 
    NSLog(@"1");  
});

NSLog(@"2");

这段代码首先获取了全局队列,也就是说,dispatch_async当中的任务被丢到了另一个线程里去执行,async在这里的含义是,当当前线程给子线程分配了block当中的任务之后,当前线程会立即执行,并不会发生阻塞,也就是异步的。那么,输出结果不是12就是21,因为我们没法把控两个线程RunLoop里到底是怎么执行的。

类似的,还有一个“同步”方法dispatch_sync,代码如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_sync(queue, ^{
NSLog(@"1");
}); 
NSLog(@"2");

这就意味着,当主线程将任务分给子线程后,主线程会等待子线程执行完毕,再继续执行自身的内容,那么结果显然就是12了。

需要注意的一点是,这里用的是全局队列,那如果把dispatch_sync的队列换成主线程队列会怎么样呢:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
    NSLog(@"1");
});

这段代码会发生死锁,因为:

  1. 主线程通过dispatch_sync把block交给主队列后,会等待block里的任务结束再往下走自身的任务,
  2. 而队列是先进先出的,block里的任务也在等待主队列当中排在它之前的任务都执行完了再走自己。

这种循环等待就形成了死锁。所以在主线程当中使用dispatch_sync将任务加到主队列是不可取的。

创建队列

我们可以使用系统提供的函数获取主串行队列和全局并行队列,当然也可以自己手动创建串行和并行队列,代码为:

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);

在MRC下,手动创建的队列是需要释放的

dispatch_release(myConcurrentDispatchQueue);

手动创建的队列和默认优先级全局队列优先级等同,如果需要修改队列的优先级,需要:

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.Steak.GCD", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t targetQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(myConcurrentDispatchQueue, targetQueue);

上面的代码修改队列的优先级为后台级别,即与默认的后台优先级的全局队列等同。

串行、并行队列与读写安全性

在向串行队列(SerialDispatchQueue)当中加入多个block任务后,一次只能同时执行一个block,如果生成了n个串行队列,并且向每个队列当中都添加了任务,那么系统就会启动n个线程来同时执行这些任务。

对于串行队列,正确的使用时机,是在需要解决数据/文件竞争问题时使用它。比如,我们可以令多个任务同时访问一块数据,这样会出现冲突,也可以把每个操作都加入到一个串行队列当中,因为串行队列一次只能执行一个线程的任务,所以不会出现冲突。

但是考虑到串行队列会因为上下文切换而拖慢系统性能,所以我们还是很期望采用并行队列的,来看下面的示例代码:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_async(queue, ^{
    //数据读取
});
dispatch_async(queue, ^{
    //数据读取2 
}); 
dispatch_async(queue, ^{
    //数据写入
});
dispatch_async(queue, ^{ 
    //数据读取3
});
dispatch_async(queue, ^{
    //数据读取4
});

显然,这5个操作的执行顺序是我们无法预期的,我们希望在读取1和读取2执行结束后,再执行写入,写入完成后再执行读取3和读取4。

为了实现这个效果,这里可以使用GCD的另一个API:

dispatch_barrier_async(queue, ^{
    //数据写入
});

这样就保证的写入操作的并发安全性。

对于没有数据竞争的并行操作,则可以使用并行队列(CONCURRENT)来实现。

JOIN行为

CGD利用dispatch_group_wait来实现多个操作的join行为,代码如下:

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, ^{
    sleep(0.5);
    NSLog(@"1"); 
});
dispatch_group_async(group, queue, ^{
    sleep(1.5);
    NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
    sleep(2.5);
    NSLog(@"3");
});
NSLog(@"aaaaa");

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC);
if (dispatch_group_wait(group, time) == 0) { 
    NSLog(@"已经全部执行完毕");
}else {
    NSLog(@"没有执行完毕");

NSLog(@"bbbbb");

这里起了3个异步线程放在一个组里,之后通过dispatch_time_t创建了一个超时时间(2秒),程序之后行,立即输出了aaaaa,这是主线程输出的,当遇到dispatch_group_wait时,主线程会被挂起,等待2秒,在等待的过程当中,子线程分别输出了1和2,2秒时间达到后,主线程发现组里的任务并没有全部结束,然后输出了bbbbb。

在这里,如果超时时间设置得比较长(比如5秒),那么会在2.5秒时第三个任务结束后,立即输出bbbbb,也就是说,当组中的任务全部执行完毕时,主线程就不再被阻塞了。

如果希望永久等待下去,时间可以设置为DISPATCH_TIME_FOREVER。

并行循环

类似于C#的PLINQ,OC也可以让循环并行执行,在GCD当中有一个dispatch_apply函数:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(20, queue, ^(size_t i) { 
    NSLog(@"%lu", i); 
});

这段代码让i并行循环了20次,如果内部处理的是一个数组,就可以实现对数组的并行循环了,它的内部是dispatch_sync的同步操作,所以在执行这个循环的过程当中,当前线程会被阻塞。

暂停和恢复

使用dispatch_suspend(queue)可以暂停队列中任务的执行,使用dispatch_result(queue)可以继续执行被暂停的队列。

2016/11/17 posted in  GCD

GCD的使用

GCD介绍

(一): 基本概念和Dispatch Queue

什么是GCD?
Grand Central Dispatch或者GCD,是一套低层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD比之NSOpertionQueue更底层更高效,并且它不是Cocoa框架的一部分。
除了代码的平行执行能力,GCD还提供高度集成的事件控制系统。可以设置句柄来响应文件描述符、mach ports(Mach port 用于 OS X上的进程间通讯)、进程、计时器、信号、用户生成事件。这些句柄通过GCD来并发执行。
GCD的API很大程度上基于block,当然,GCD也可以脱离block来使用,比如使用传统c机制提供函数指针和上下文指针。实践证明,当配合block使用时,GCD非常简单易用且能发挥其最大能力。
你可以在Mac上敲命令“man dispatch”来获取GCD的文档。
为何使用?
GCD提供很多超越传统多线程编程的优势:
易用: GCD比之thread跟简单易用。由于GCD基于work unit而非像thread那样基于运算,所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。基于block的血统导致它能极为简单得在不同代码作用域之间传递上下文。
效率: GCD被实现得如此轻量和优雅,使得它在很多地方比之专门创建消耗资源的线程更实用且快速。这关系到易用性:导致GCD易用的原因有一部分在于你可以不用担心太多的效率问题而仅仅使用它就行了。
性能: GCD自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。

Dispatch Objects

尽管GCD是纯c语言的,但它被组建成面向对象的风格。GCD对象被称为dispatch object。Dispatch object像Cocoa对象一样是引用计数的。使用dispatch_release和dispatch_retain函数来操作dispatch object的引用计数来进行内存管理。但主意不像Cocoa对象,dispatch object并不参与垃圾回收系统,所以即使开启了GC,你也必须手动管理GCD对象的内存。
Dispatch queues 和 dispatch sources(后面会介绍到)可以被挂起和恢复,可以有一个相关联的任意上下文指针,可以有一个相关联的任务完成触发函数。可以查阅“man dispatch_object”来获取这些功能的更多信息。

Dispatch Queues

GCD的基本概念就是dispatch queue。dispatch queue是一个对象,它可以接受任务,并将任务以先到先执行的顺序来执行。dispatch queue可以是并发的或串行的。并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行,串行队列同一时间只执行单一任务。

GCD中有三种队列类型:

The main queue: 与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。
Global queues: 全局队列是并发队队列,并由整个进程共享。进程中存在三个全局队列:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入优先级来访问队列。
用户队列: 用户队列 (GCD并不这样称呼这种队列, 但是没有一个特定的名字来形容这种队列,所以我们称其为用户队列) 是用函数 dispatch_queue_create 创建的队列. 这些队列是串行的。正因为如此,它们可以用来完成同步机制, 有点像传统线程中的mutex。

创建队列
要使用用户队列,我们首先得创建一个。调用函数dispatch_queue_create就行了。函数的第一个参数是一个标签,这纯是为了debug。Apple建议我们使用倒置域名来命名队列,比如“com.dreamingwish.subsystem.task”。这些名字会在崩溃日志中被显示出来,也可以被调试器调用,这在调试中会很有用。第二个参数目前还不支持,传入NULL就行了。

dispatch队列的生成可以有这几种方式:
1. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.serial", DISPATCH_QUEUE_SERIAL); //生成一个串行队列,队列中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。第一个参数是队列的名称,在调试程序时会非常有用,所有尽量不要重名了。
2. dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent", DISPATCH_QUEUE_CONCURRENT); //生成一个并发执行队列,block被分发到多个线程去执行
3. dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //获得程序进程缺省产生的并发队列,可设定优先级来选择高、中、低三个优先级队列。由于是系统默认生成的,所以无法调用dispatch_resume()和dispatch_suspend()来控制执行继续或中断。需要注意的是,三个队列不代表三个线程,可能会有更多的线程。并发队列可以根据实际情况来自动产生合理的线程数,也可理解为dispatch队列实现了一个线程池的管理,对于程序逻辑是透明的。
官网文档解释说共有三个并发队列,但实际还有一个更低优先级的队列,设置优先级为DISPATCH_QUEUE_PRIORITY_BACKGROUND。Xcode调试时可以观察到正在使用的各个dispatch队列。
4. dispatch_queue_t queue = dispatch_get_main_queue(); //获得主线程的dispatch队列,实际是一个串行队列。同样无法控制主线程dispatch队列的执行继续或中断。

多核心的性能

为了在单一进程中充分发挥多核的优势,我们有必要使用多线程技术(我们没必要去提多进程,这玩意儿和GCD没关系)。在低层,GCD全局dispatch queue仅仅是工作线程池的抽象。这些队列中的Block一旦可用,就会被dispatch到工作线程中。提交至用户队列的Block最终也会通过全局队列进入相同的工作线程池(除非你的用户队列的目标是主线程,但是为了提高运行速度,我们绝不会这么干)。
有两种途径来通过GCD“榨取”多核心系统的性能:将单一任务或者一组相关任务并发至全局队列中运算;将多个不相关的任务或者关联不紧密的任务并发至用户队列中运算;
全局队列
设想下面的循环:

for(id obj in array)
    [self doSomethingIntensiveWith:obj];

假定 -doSomethingIntensiveWith: 是线程安全的且可以同时执行多个.一个array通常包含多个元素,这样的话,我们可以很简单地使用GCD来平行运算:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for(id obj in array)
    dispatch_async(queue, ^{
        [self doSomethingIntensiveWith:obj];
    });

如此简单,我们已经在多核心上运行这段代码了。

dispatch group
当然这段代码并不完美。有时候我们有一段代码要像这样操作一个数组,但是在操作完成后,我们还需要对操作结果进行其他操作

for(id obj in array)
    [self doSomethingIntensiveWith:obj];
[self doSomethingWith:array];

这时候使用GCD的 dispatch_async 就悲剧了.我们还不能简单地使用dispatch_sync来解决这个问题, 因为这将导致每个迭代器阻塞,就完全破坏了平行计算。
解决这个问题的一种方法是使用dispatch group。一个dispatch group可以用来将多个block组成一组以监测这些Block全部完成或者等待全部完成时发出的消息。使用函数dispatch_group_create来创建,然后使用函数dispatch_group_async来将block提交至一个dispatch queue,同时将它们添加至一个组。所以我们现在可以重新编码:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
for(id obj in array)
    dispatch_group_async(group, queue, ^{
        [self doSomethingIntensiveWith:obj];
    });
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
  
[self doSomethingWith:array];

如果这些工作可以异步执行,将函数-doSomethingWith:放在后台执行。我们使用dispatch_group_async函数建立一个block在组完成后执行:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
for(id obj in array)
    dispatch_group_async(group, queue, ^{
        [self doSomethingIntensiveWith:obj];
    });
dispatch_group_notify(group, queue, ^{
    [self doSomethingWith:array];
});
dispatch_release(group);

不仅所有数组元素都会被平行操作,后续的操作也会异步执行,并且这些异步运算都会将程序的其他部分的负载考虑在内。注意如果-doSomethingWith:需要在主线程中执行,比如操作GUI,那么我们只要将main queue而非全局队列传给dispatch_group_notify函数就行了

dispatch_apply
对于同步执行,GCD提供了一个简化方法叫做dispatch_apply。这个函数调用单一block多次,并平行运算,然后等待所有运算结束,就像我们想要的那样:

spatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index){
        [self doSomethingIntensiveWith:[array objectAtIndex:index]];
    });
    [self doSomethingWith:array];

这很棒,但是异步咋办?dispatch_apply函数可是没有异步版本的。但是我们使用的可是一个为异步而生的API啊!所以我们只要用dispatch_async函数将所有代码推到后台就行了:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    dispatch_apply([array count], queue, ^(size_t index){
        [self doSomethingIntensiveWith:[array objectAtIndex:index]];
    });
    [self doSomethingWith:array];
});

dispatch source
何为Dispatch Sources
简单来说,dispatch source是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个block放入一个dispatch queue的执行例程中。

2016/7/18 posted in  GCD