GCD多线程

线程&多线程

本篇示例代码,结合示例学习,更容易理解。

线程到底是什么呢?我们来温习一下。先看一下下面的OC源代码。

int main(int argc, char * argv[]) {
    id o = [[NSObject alloc] init];
    
    [o class];
    
    return 0;
}

虽然调用了几个方法,但代码基本上是按从上到下的顺序执行的。
那么,该源代码在Mac或者iPhone上是如何执行的呢?
该源代码通过编译器转换为如下的CPU指令列(二进制代码)。

->  0x102cb9190 <+0>:   pushq  %rbp
    0x102cb9191 <+1>:   movq   %rsp, %rbp
    0x102cb9194 <+4>:   subq   $0x20, %rsp
    0x102cb9198 <+8>:   movl   $0x0, -0x4(%rbp)
    0x102cb919f <+15>:  movl   %edi, -0x8(%rbp)
    0x102cb91a2 <+18>:  movq   %rsi, -0x10(%rbp)
    0x102cb91a6 <+22>:  movq   0x3253(%rip), %rax        
    0x102cb91ad <+29>:  movq   %rax, %rdi
    0x102cb91b0 <+32>:  callq  0x102cb9484               
    0x102cb91b5 <+37>:  movq   0x322c(%rip), %rsi        
    0x102cb91bc <+44>:  movq   %rax, %rdi
    0x102cb91bf <+47>:  callq  *0x1e43(%rip)            
    0x102cb91c5 <+53>:  movq   %rax, -0x18(%rbp)
    0x102cb91c9 <+57>:  movq   -0x18(%rbp), %rdi
    0x102cb91cd <+61>:  movq   0x321c(%rip), %rsi        
    0x102cb91d4 <+68>:  callq  *0x1e2e(%rip)             
    0x102cb91da <+74>:  xorl   %ecx, %ecx
    0x102cb91dc <+76>:  movl   %ecx, %esi
    0x102cb91de <+78>:  movl   $0x0, -0x4(%rbp)
    0x102cb91e5 <+85>:  leaq   -0x18(%rbp), %rdi
    0x102cb91e9 <+89>:  movq   %rax, -0x20(%rbp)
    0x102cb91ed <+93>:  callq  0x102cb949c               
    0x102cb91f2 <+98>:  movl   -0x4(%rbp), %eax
    0x102cb91f5 <+101>: addq   $0x20, %rsp
    0x102cb91f9 <+105>: popq   %rbp
    0x102cb91fa <+106>: retq 

汇集CPU命令列和数据,将其作为一个应用程序安装到Mac 或 iPhone上。
iPhone的操作系统iOS根据用户的指示启动该应用程序后,首先便将包含在该应用程序的命令列配置到内存中。CPU从应用程序指定的地址开始,一个一个地执行CPU命令列,先执行190,再执行191,再执行194以此不断循环下去。
OC的if elsefor等控制语言或函数调用会让命令执行地址远离当前位置(位置迁移),单由于CPU以此只能执行一个命令,不能执行某处分开的并行的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,其执行不会出现分歧。如下图

image.png

这里所说的”1个CPU执行的CPU命令为一条无分叉路径“即为”线程“。
这种无分叉路径不只1条,存在多条时即为”多线程“


image.png

摘自:《Objective-C高级编程-iOS与OS X多线程和内存管理》3.1.2多线程编程

GCD

什么是GCD

Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现、开发者只需要定义想好执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。

摘自苹果官方说明

GCD的优势

  • GCD 是苹果公司为多核并行运算提出的解决方案
  • GCD 会自动利用更多的CPU内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。将任务添加到队列并指定任务执行的函数。

GCD的API

1.队列(Dispatch Queue)

同步队列和异步队列

串行队列(Serial Dispatch Queue):等待现在执行中处理结束,系统对一个串行队列就只生成并使用一个线程。但是如果生成多个串行队列就会对应生成多个线程。
并发队列(Concurrent Dispatch Queue):不等待现在执行中处理结束,一个并发队列可以生成并使用多个线程。
只在为了避免多线程编程问题之一———多个线程更新相同资源导致数据竞争时使用Serial Dispatch Queue(串行队列)。

2.dispatch_queue_create

第一种方法是通过GCDAPI生成Dispatch Queue。以下源代码生成了 Serial Dispatch Queue

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.demo.gcd.mySerialDispatchQueue", NULL);

该函数的第一个参数指定Serial Dispatch Queue的名称。Dispatch Queue的名称推荐使用应用程序ID这种逆序全程域名。该名称在XcodeInstrument的调试器中作为Dispatch Queue名称表示。另外该名称也出现在应用程序崩溃时所生成的CrashLog中。
第二个参数指定为NULL或者DISPATCH_QUEUE_SERIAL则生成Serial Dispatch Queue (串行调度队列),如果要生成Concurrent Dispatch Queue (并行调度队列)则把第二个参数指定为DISPATCH_QUEUE_CONCURRENT
注意点:

  • 一旦生成Serial Dispatch Queue并追加处理,系统对于一个Serial Dispatch Queue就只生成并使用一个线程。如果生成2000个Serial Dispatch Queue 就会产生2000个线程,这样就会消耗大量的内存,引发大量的上下文切换,大幅降低系统的响应性能,因此生成个数应当仅限所需的数量。例如更新数据库时,一个表生成一个Serial Dispatch Queue,更新文件时一个文件或者可以分割的一个文件块生成一个Serial Dispatch Queue
  • 当想并行执行不发生数据竞争等问题的处理时,使用 Concurrent Dispatch Queue。而且对于Concurrent Dispatch Queue来说,不管生成多少个,由于XNU内核只使用有效管理的线程,一次不会发生Serial Dispatch Queue的问题。

Main Dispatch Queue/Global Dispatch Queue

Main Dispatch Queue正如其名称中含有的”Main“一样,是在主线程中执行的Dispatch Queue。因为主线程只有1个,所以Main Dispatch Queue自然就是Serial Dispatch Queue。
Global Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue 它有4个优先级。通过XNU内核用于Global Dispatch Queue的线程并不能保证实时性,因此执行优先级只是大概判断。例如在处理内容的执行可有可无时,使用后台优先级,只能进行这种程度的区分。

名称 Dispatch Queue类型 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue(High Priority) Concurrent Dispatch Queue 执行优先级:高(最高优先级)
Global Dispatch Queue(Default Priority) Concurrent Dispatch Queue 执行优先级:默认
Global Dispatch Queue(Low Priority) Concurrent Dispatch Queue 执行优先级:低
Global Dispatch Queue(Background Priority) Concurrent Dispatch Queue 执行优先级:后台

获取方式

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_set_target_queue

dispatch_queue_create函数生成的Dispatch Queue不管是Serial Dispatch Queue 还是 Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。而变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue函数。在后台执行动作处理的Serial Dispatch Queue的生成方法如下:

dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.lxc.GCD.serialQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

指定变更执行优先级的Dispatch Queue为第一个参数,指定与要使用的执行优先级相同优先级的Global Dispatch Queue为第二个参数(目标),第一个参数如果指定为系统的Main Dispatch Queue 或 Global Dispatch Queue则不知道会出现什么情况,因此不能指定。
同时dispatch_set_target_queue还能设置层级,比如多个Serial Dispatch Queue指定目标为同一个Serial Dispatch Queue,那么原来本应该并行执行的多个Serial Dispatch Queue在目标Serial Dispatch Queue上只能同时执行一个处理。

dispatch_after

 dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"waited at least three seconds.");
    });

需要注意的是,dispatch_after函数并不是在指定的时间后执行,而只是在指定时间追加处理到Dispatch Queue。此源码与在3秒后用dispatch_async函数追加Block到Main Dispatch Queue 相同。

dispatch_sync

dispatch_async函数的”async“意味着”非同步”(asynchronous),就是将指定的Block“非同步”地追加到指定的Dispatch Queue中。dispatch_async 函数不做任何等待。
既然有“async”,当然也有“sync”,即 dispatch_sync 函数。它意味着“同步”(synchronous),也就是将指定的Block“同步”追加到指定的Dispatch Queue中,在追加Block结束之前,dispatch_sync函数会一直等待。“等待”意味着当前线程停止。

调度组(Dispatch Group)

在追加到Dispatch Queue中的多个处理全部结束后想要执行结束处理,这种情况经常出现。只使用一个Serial Dispatch Queue时,只要将想执行的处理全部追加到Serial Dispatch Queue中并在最后追加结束处理,即可实现。但是如果是使用Concurrent Dispatch Queue 或者同时使用多个Dispatch Queue时,源代码就会变的很复杂,此种情况可以使用Dispatch Group。

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_notify(group, queue, ^{
        NSLog(@"done!");
    });

在追加到Dispatch Queue中的处理全部执行结束时,该源码中使用dispatch_group_notify函数会将执行的Block追加到Dispatch Queue中。另外,在Dispatch Group中也可以使用dispatch_group_wait函数仅等待全部处理执行结束。

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

这里第二个参数指定为等待的时间(超时时间),DISPATCH_TIME_FOREVER意味着永久等待,这里的等待是什么意思呢?这意味着一旦调用dispatch_group_wait函数,该函数就处于调用状态而不返回。即执行dispatch_group_wait函数的现在的线程(当前线程)停止。在经过dispatch_group_wait函数中指定的时间或属于指定Dispatch Group的处理全部执行结束之前,执行该函数的线程停止。

栅栏函数(dispatch_barrier_async)

在访问数据库或文件时,如前所述,使用Serial Dispatch Queue可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只与读取处理并行执行,那个多个读取处理并行执行不会发生问题。
也就是说为了高效地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理结束之前,读取处理不可执行)。这种情况使用前面所讲的内容实现起来就比较麻烦,就需要用到dispatch_barrier_async。

    dispatch_async(queue, blk0_for_reading);
    dispatch_async(queue, blk1_for_reading);
    dispatch_async(queue, blk2_for_reading);
    dispatch_barrier_async(queue, blk_for_writing);
    dispatch_async(queue, blk3_for_reading);
    dispatch_async(queue, blk4_for_reading);
    dispatch_async(queue, blk5_for_reading);

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


Dispatch_barrier_async函数处理流程
  • 注意:栅栏函数不能用在Global Dispatch Queue中,用到Global中会不起作用

信号量(Dispatch Semaphore)

Dispatch Semaphore是持有计数的信号,该计数是多线程编程的计数类型信号。所谓信号,类似于过马路常用的手旗。可以通过时举起手旗,不可通过时放下手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或者大于1时,减去1而不等待。
下面介绍一下使用方法。通过dispatch_semaphore_create 函数生成Dispatch Semaphore。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

参数表示计数的初始值。

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

dispatch_semaphore_wait函数等待Dispatch Semaphore的计数值达到大于或等于1。当计数值大于或等于1,对该计数进行减法并从dispatch_semaphore_wait函数返回。第二个参数是等待时间。

dispatch_semaphore_signal(semaphore);

dispatch_semaphore_signal函数将Dispatch Semaphore的计数值加1。

- (void)dispatchSemaphoreTest{
    dispatch_queue_t queue = dispatch_queue_create("com.test.semaphore", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    NSMutableArray *mArr = [NSMutableArray array];
    
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [mArr addObject:@(i)];
            NSLog(@"%d",i);
            dispatch_semaphore_signal(semaphore);
        });
    }
}

因为该源代码使用Concurrent Dispatch Queue 更新NSMutableArray类对象,所以如果不添加信号量控制执行后由内存错误导致的应用程序异常结束的概率很高,此时应使用Dispatch Semaphore 。

你可能感兴趣的:(GCD多线程)