iOS进阶之路 (十六)多线程 - GCD

1,758 阅读19分钟

GCD(Grand Central Dispatch) 是基于C语言的API,是苹果公司为多核的并行运算提出的解决方案。GCD会自动利用更多的CPU内核(比如双核、四核)。程序员只需要将任务添加到队列中,并且指定执行任务的函数,不需要别写任何线程管理的代码。

学习 GCD 之前,先来了解 GCD 中两个核心概念:任务队列

一. 任务 - Task

任务:就是执行操作的意思,通俗的说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。

执行任务有两种方式:同步执行异步执行。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力

  1. 同步任务 dispatch_sync
  • 同步添加任务到指定的队列中,必须等待当前队列的任务执行完毕,才能执行下一个任务
  • 只能在当前线程中执行任务,不具备开启新线程的能力。
  1. 异步任务 dispatch_async
  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力
  • 注意:异步执行虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(下面会讲)

二. 队列 - Dispatch Queue

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(First - In -First - Out)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。队列的结构可参考下图:

在 GCD 中有两种队列:串行队列并发队列。两者都符合 FIFO 原则。两者的主要区别是:执行顺序不同,以及开启线程数不同

  1. 串行队列 Serial Dispatch Queue 每次只有一个 任务 被执行,只申请一个 线程 ,一个 任务 执行完毕,再执行下一个

2. 并发队列 Concurrent Dispatch Queue 可以申请多个 线程,让多个 任务 同时执行(注意:并发队列的并发功能只有在 异步dispatch_async 方法下有效)

三. GCD的使用

GCD使用很简单,只有三步

  • 创建 任务(同步任务 或者 异步任务)
  • 创建 队列(串行队列 或者 并发队列)
  • 任务 追加到任务的等待 队列 中,然后系统就会根据任务类型执行任务(同步执行 或者 异步执行)

3.1 任务的创建

同步执行任务的创建方法 dispatch_sync 和 异步执行任务的创建方法 dispatch_async

// 同步任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步任务代码
});

// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步任务代码
});

3.2 队列的创建

GCD 可以使用 dispatch_queue_creat 创建队列

dispatch_queue_t queue - dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t  _Nullable attr#>);
  • const char * _Nullable label: 队列的唯一标识符,用于debug,可以为空。
  • dispatch_queue_attr_t _Nullable attr: 队列种类。DISPATCH_QUEUE_SERIAL 串行队列,DISPATCH_QUEUE_CONCURRENT 并发队列。

第二个参数如果传NULL,会创建串行队列。

主队列

对于串行队列,GCD默认提供了:主队列 - Main Dispatch Queue

  • 所有放在主队列的任务,都会放在主线程执行
  • 可以使用 dispatch_get_main_queue() 方法获得主队列

主队列本质就是一个普通的串行队列,只是默认情况下,代码放在主队列中,主队列的代码会放到主程序中执行,给我们主队列特殊的感觉。

全局并发队列

对于并发队列,GCD默认提供了:全局并发队列 - Global Dispatch Queue。可以使用 dispatch_get_global_queue() 方法获得全局并发队列

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

dispatch_queue_t queue = dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)
  • long identifier : 标示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT
  • 暂时没用,可以传0

4.任务 和 队列 不同组合方式

4.1 同步任务 + 串行队列

- (void)syncSerial
{
    NSLog(@"主线程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_sync(queue, ^{
            NSLog(@"同步任务 + 串行队列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}

  • 所有任务都在当前线程(主线程)中执行,没有开启新的线程。(同步任务只能在当前线程中执行任务,不具备开启新线程的能力)
  • 所有打印都在 ---begin--- 和 ---end--- 之间执行(同步任务需要等待队列的任务执行结束)
  • 任务按顺序执行(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行)。

4.2 异步任务 + 串行队列

- (void)asyncSerial
{
    NSLog(@"主线程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_async(queue, ^{
            NSLog(@"异步任务 + 串行队列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}

  • 开启了一条新线程(异步任务具有开辟线程的能力,串行队列只开启一个线程
  • 所有任务在 ---begin--- 和 ---end--- 之后执行(异步执行不会做任何等待,可以继续执行接下来的任务
  • 任务按顺序执行(串行队列每次只有一个任务被执行,任务一个接一个按顺序执行

4.3 同步任务 + 并行队列

- (void)syncConcurrent
{
    NSLog(@"主线程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_sync(queue, ^{
            NSLog(@"同步任务 + 并行队列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}

  • 所有任务都在当前线程(主线程)中执行,没有开辟细腻的线程(同步任务只能在当前线程中执行任务,不具备开启新线程的能力
  • 所有打印都在 ---begin--- 和 ---end--- 之间执行(同步任务需要等待队列的任务执行结束)
  • 任务按顺序执行:原因:虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。

4.4 异步任务 + 并行队列

- (void)asyncConcurrent
{
    NSLog(@"主线程 - %@", [NSThread currentThread]);
    NSLog(@"---begin---");
    
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i = 0; i < 10; i ++) {
        dispatch_async(queue, ^{
            NSLog(@"异步任务 + 并行队列:%ld -- %@", (long)i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"---end---");
}

  • 除了当前线程(主线程),系统又开启了多 个线程,并且任务是交替/同时执行的。(异步任务具备开启新线程的能力。 并发队列 可开启多个线程,同时执行多个任务)。
  • 所有任务在 ---begin--- 和 ---end--- 之后执行(异步执行不会做任何等待,可以继续执行接下来的任务

4.5 同步任务 + 主队列

同步任务 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。

  1. 主线程调用 同步任务 + 主队列

崩溃原因:主队列是串行队列 所有放在主队列中的任务,都会放到主线程中执行, 当我们在主线程 中执行syncMain方法,相当于把 syncMain任务 放到了 主线程 的队列中。而 同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务1 追加到主队列中,任务1 就在等待主线程处理完 syncMain任务。而 syncMain任务 需要等待 任务1 执行完毕,才能接着执行。两个任务 相互等待 对方执行完毕。

  1. 子线程调用 同步任务 + 主队列
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];

  • 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行) 所有打印都在 ---begin--- 和 ---end--- 之间执行(同步任务需要等待队列的任务执行结束
  • 任务是按顺序执行的(主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行

为什么现在就不会卡住了呢?

因为 syncMain任务 放到 子线程 里; 而 任务1到10 都在追加到主队列中,会在 主线程 中执行。主队列现在没有正在执行的任务,所以会直接执行主队列的 任务1 ,等 任务1 执行完毕,再接着执行 任务2。所以这里不会卡住线程,也就不会造成死锁问题。

4.6 嵌套使用

以下demo默认都在主队列,因为主队列是串行队列,代码自上到下依次执行。默认异步操作都是长耗时操作。

  1. 串行异步中嵌套同步
- (void)demo1
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

打印顺序:1 5 2 死锁

  • 代码块B为同步任务,等待,产生阻塞。所以,任务4要等到代码块B执行完毕才能执行。
  • 代码块B 要执行完毕,必须等到 任务3 执行完毕.
  • 因为是串行队列,遵循FIFO。任务3 要等到 任务4 执行完毕才执行
  • 所以,任务4块B块B任务3任务3任务4。死锁。
  1. 串行同步中嵌套异步
- (void)demo2
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_sync(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
打印顺序: 12453 
  • 主队列,代码依次执行:队列中依次加入任务1 代码块A 任务5代码块A 是同步任务,等待。 所以打印 1 代码块A 5
  • 代码块A内: 队列中依次加入 任务2 代码块B 任务4代码块B是异步任务,不等待,耗时。所以打印 2 4 3
  • 代码块B 执行完毕后 打印5
  • 同步操作保证了 4一定在5之前打印
  1. 并行异步中嵌套同步
- (void)demo3
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
打印结果:15234
  • 主队列,代码依次执行:队列中依次加入任务1 代码块A 任务5代码块A是异步任务,不等待,耗时。所以打印 1 5
  • 代码块A内:并行队列,申请新线程。新线程依次加入任务2 代码块B 任务4代码块B同步任务,等待阻塞当前子线程。打印 2 3 4
  • 同步操作保证了 3 一定在 4 之前打印
  1. 并行同步中嵌套异步
- (void)demo4
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_sync(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

打印结果:12453
  • 主队列,代码依次执行:队列中依次加入任务1 代码块A 任务5代码块A是同步任务,等待。所以打印 1 代码块A 5
  • 代码块A内:并行队列,加入 任务2 代码块B 任务4代码块B异步任务,不等待,耗时,申请新的线程。
  • 同步操作保证 4 一定在 5 之前打印

5. GCD的应用

5.1 dispatch_after 延时执行方法

应用场景:在指定时间之后执行某个任务。

dispatch_after 方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 方法是很有效的。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"2秒后输出");
});

5.2 dispatch_once 一次性代码

应用场景:单例,method-Swizzling

dispatch_once 能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行 1 次的代码(这里面默认是线程安全的)
    });
}

5.3 dispatch_apply 快速迭代

应用场景:获得网络数据后提前算出各个控件的大小

dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

  • 串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。但是这样就体现不出快速迭代的意义了。
  • 我们可以利用并发队列进行异步执行。比如说遍历 0~5 这 6 个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。
- (void)apply
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_SERIAL);
    NSLog(@"dispatch_apply前");
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"dispatch_apply -- 线程%zu-%@", index, [NSThread currentThread]);
    });
    NSLog(@"dispatch_apply后");
}

无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕, ,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

5.4 dispatch_barrier 栅栏函数

应用场景:线程同步

异步任务+并发队列会开辟线程,多个任务同时进行,各任务也会因为任务复杂度和cpu的调度导致执行完毕乱序。如何设置让任务顺序执行完毕,栅栏函数就是很好的解决办法。

  • dispatch_barrier_async 用于提交异步执行栅栏函数块任务,并立即返回。该函数不会阻塞线程,只会阻塞任务执行。栅栏函数提交之后并不会立即执行,而是会直接返回。等待在栅栏函数之前提交的任务都执行完成之后,栅栏函数任务会自动执行,在栅栏函数之后提交的任务必须在栅栏函数执行完成之后才会执行.

  • dispatch_barrier_sync 用于提交同步执行栅栏函数块任务,并不会立即返回,需要等待栅栏函数提交的任务执行完毕之后才会返回。与dispatch_barrier_async不同,由于该函数需要同步执行,在该栅栏函数任务执行完成之前,函数不会返回,所以此时后边的任务都会被阻塞.

  1. 测试1:
- (void)barrierSync
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_barrier_sync(queue, ^{
        NSLog(@"dispatch_barrier_sync -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
    });
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}

  1. 测试2
- (void)barrierAsync
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"dispatch_barrier_async -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
    });
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}

比较 测试1测试2

  1. dispatch_barrier_syncdispatch_barrier_async 的共同点:
  • 都会等待在它前面插入队列的任务(任务begin 任务1)先执行完。
  • 都会等待他们自己的任务(barrier operation)执行完之后再执行后面的任务(任务2 任务end)。
  1. dispatch_barrier_syncdispatch_barrier_async 的不同点:
  • 将任务插入到队列时,dispatch_barrier_sync 需要等待自己的任务(barrier operation)执行完毕后,才会插入后面的任务(任务2 任务end),然后执行后面的任务。所以 任务endbarrier operation 后打印。说白了,会阻塞线程。
  • dispatch_barrier_async 将自己的任务插入到队列之后,不会等待自己的任务(barrier operation)执行结果,它会继续插入后面的任务。所以 任务end 先打印。

  1. 测试3:dispatch_barrier_sync线程任务线程 不一致,会怎样?
- (void)barrierSync
{
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    // 新建线程
    dispatch_queue_t queue2 = dispatch_queue_create("com.akironer2", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
    });
    
    // 阻塞新建线程
    dispatch_barrier_sync(queue2, ^{
        NSLog(@"dispatch_barrier_sync -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
    });
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}

使用 dispatch_barrier_async 测试,也有同样的结果

  • barrier的本质是阻塞线程。dispatch_barrier 线程任务线程 不一致,barrier没有发挥作用。
  • 所以我们使用AFNetworking做网络请求时,不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列。
  1. 测试4:对全局并发队列使用栅栏函数,有什么效果?

  • 对全局并发队列使用栅栏函数, 可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃。

5.5 dispatch_group 调度组

应用场景:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。

5.5.1 dispatch_group_async & dispatch_group_notify

  • dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中
  • dispatch_group_notify 监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务group 中,并执行任务。
- (void)groupAsync
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    dispatch_group_async(group, queue, ^{
        sleep(5);
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(4);
        NSLog(@"任务3 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"end -- %@", [NSThread currentThread]);
    });
}

打印:
22:23:12.180761+0800 GCD[2123:330417] begin -- <NSThread: 0x60000212ad40>{number = 1, name = main}
22:23:15.182107+0800 GCD[2123:330584] 任务2 -- <NSThread: 0x600002150080>{number = 3, name = (null)}
22:23:16.180985+0800 GCD[2123:330585] 任务3 -- <NSThread: 0x60000215c180>{number = 4, name = (null)}
22:23:17.182406+0800 GCD[2123:330596] 任务1 -- <NSThread: 0x600002122ec0>{number = 5, name = (null)}
22:23:17.182667+0800 GCD[2123:330596] end -- <NSThread: 0x600002122ec0>{number = 5, name = (null)}

5.5.2 dispatch_group_wait

暂停当前线程,等待指定的group中的任务执行完成之后,才往下执行任务。相比于dispatch_group_notify, dispatch_group_wait 会阻塞线程。

- (void)groupWait
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_group_async(group, queue, ^{
        sleep(5);
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        sleep(4);
        NSLog(@"任务3 -- %@", [NSThread currentThread]);
    });
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"end -- %@", [NSThread currentThread]);
}

打印:
22:31:45.390633+0800 GCD[2157:336097] begin -- <NSThread: 0x6000001d6d00>{number = 1, name = main}
22:31:48.392504+0800 GCD[2157:336257] 任务2 -- <NSThread: 0x6000001a4640>{number = 6, name = (null)}
22:31:49.392455+0800 GCD[2157:336255] 任务3 -- <NSThread: 0x6000001dc400>{number = 5, name = (null)}
22:31:50.395577+0800 GCD[2157:336256] 任务1 -- <NSThread: 0x6000001ac380>{number = 4, name = (null)}
22:31:50.395978+0800 GCD[2157:336256] end -- <NSThread: 0x6000001ac380>{number = 4, name = (null)}

5.5.3 dispatch_group_enter & dispatch_group_leave

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
  • 当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务。
- (void)groupWait
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        sleep(5);
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"end -- %@", [NSThread currentThread]);
    });
}

打印:
22:42:10.338809+0800 GCD[2188:343124] begin -- <NSThread: 0x60000338adc0>{number = 1, name = main}
22:42:13.340545+0800 GCD[2188:343288] 任务2 -- <NSThread: 0x6000033e8340>{number = 5, name = (null)}
22:42:15.343788+0800 GCD[2188:343293] 任务1 -- <NSThread: 0x600003304000>{number = 7, name = (null)}
22:42:15.344152+0800 GCD[2188:343293] end -- <NSThread: 0x600003304000>{number = 7, name = (null)}

5.6 dispatch_semaphore 信号量

应用场景

  • 保持线程同步,将异步执行任务转化为同步执行任务
  • 保证线程安全,为线程加锁。

信号量是基于计数器的一种多线程同步机制,用来管理对资源的并发访问。信号量内部有一个可以原子递增或递减的值。如果一个动作尝试减少信号量的值,使其小于0,那么这个动作将会被阻塞,直到有其他调用者(在其他线程中)增加该信号量的值。

  • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加 1
  • dispatch_semaphore_wait:可以使总信号量减 1。信号量小于等于 0 则会阻塞当前线程,直到信号量大于0或者经过输入的时间值;若信号量大于0,则会使信号量减1并返回,程序继续住下执行。

dispatch_semaphore_waitdispatch_semaphore_signal 这两个函数中间的执行代码,每次只会允许限定数量的线程进入,这样就有效的保证了在多线程环境下,只能有限定数量的线程进入。

注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

5.6.1 dispatch_semaphore 线程同步

- (void)semaphoreAync
{
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_queue_t queue = dispatch_queue_create("com.akironer", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d -- 线程%@", i, [NSThread currentThread]);
            // 打印任务结束后信号量解锁
            dispatch_semaphore_signal(sem);
        });
        // 异步耗时,所以这里信号量加锁
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
}

  • semaphore 初始创建时计数为 0。
  • 异步执行 将 任务1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait ,semaphore 减 1,此时 semaphore == -1,当前线程进入阻塞状态。
  • 任务1 开始执行。执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程恢复继续执行。

5.6.2 dispatch_semaphore 线程安全

- (void)saleTickets
{
    NSLog(@"begin -- %@", [NSThread currentThread]);
    
    self.ticketCount = 20;
    
    // queue1 代表售卖窗口1
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表售卖窗口2
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketsSemaphore];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketsSemaphore];
    });
}

- (void)saleTicketsSemaphore
{
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        if (self.ticketCount > 0) {
            self.ticketCount --;
            NSLog(@"剩余票数:%ld 窗口:%@", (long)self.ticketCount, [NSThread currentThread]);
            sleep(0.1);
        } else {
            NSLog(@"卖完了%@", [NSThread currentThread]);
            // 相当于解锁
            dispatch_semaphore_signal(self.semaphore);
            break;
        }
        // 相当于解锁
        dispatch_semaphore_signal(self.semaphore);
    }
}

5.6.3 dispatch_semaphore 最大并发量

- (void)dispatchAsyncLimit
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    
    //任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务1执行--%@", [NSThread currentThread]);
        sleep(1);
        NSLog(@"任务1完成--%@", [NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务2执行--%@", [NSThread currentThread]);
        sleep(1);
        NSLog(@"任务2完成--%@", [NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"任务3执行--%@", [NSThread currentThread]);
        sleep(1);
        NSLog(@"任务3完成--%@", [NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
}
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/26/171b23c361a2d077~tplv-t2oaga2asx-image.image)

由于设定的信号值为2,先执行2个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2

5.7 dispatch_source

应用场景:GCDTimer

NSTimer是依赖Runloop的,而Runloop可以运行在不同的模式下。如果Runloop在阻塞状态,NSTimer触发时间就会推迟到下一个Runloop周期,因此NSTimer在计时上会有误差。而GCD定时器不依赖Runloop,计时精度要高很多。

// 强持有
@property (nonatomic, strong) dispatch_source_t timer;

// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 2.创建timer
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 3.设置timer首次执行时间,间隔,精确度
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
// 4.设置timer事件回调
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"GCDTimer");
});
// 5.默认是挂起状态,需要手动激活
dispatch_resume(_timer);

参考资料

官方文档 -- Dispatch

满庭花醉三千客 —— 栅栏函数