文章

多线程:GCD

使用 GCD 的时候如何让线程同步,也有多种方法

  1. dispatch_group
  2. dispatch_barrier
  3. dispatch_semaphore


快速迭代方法:dispatch_apply

1
2
3
4
5
6
7
// 阻塞当前线程,多次执行 block 代码块,类似快速遍历的 for 循环,index 无序。
// 只有结合并行队列时才有意义,如果同步队列就体现不出多线程的性能优势
dispatch_apply(1000, queue, ^(size_t index){
    NSLog(@"第 %zu 次执行 === %@",index, [NSThread currentThread]);
});

NSLog(@"Done ===");



dispatch_barrier_async 栅栏

当任务需要异步进行,但是这些任务需要分成两组来执行,第一组完成之后才能进行第二组的操作。这时候就用了到 GCD 的栅栏方法 dispatch_barrier_async。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- (IBAction)barrierGCD:(id)sender {
    
    // 并发队列
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    
    // 异步并发执行
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步1   %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步2   %@",[NSThread currentThread]);
        }
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"------------barrier------------%@", [NSThread currentThread]);
        NSLog(@"******* 并发异步执行,但是 3、4 一定在 1、2 后面 *********");
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步3   %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"栅栏:并发异步4   %@",[NSThread currentThread]);
        }
    });
}

上面代码开启多条线程,所有任务都是异步并发进行。但是 1、2 完成之后,才会进行 3、4 的操作。



dispatch_group_t 队列组示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)testGroup {
    dispatch_group_t group =  dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"有一个耗时操作完成!");
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"有一个耗时操作完成!");
    });
    
    // 不阻塞线程,group 中的所有其他任务都执行完后才会执行 dispatch_group_notify 的任务
    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"队列组中的耗时操作都完成了!");
    });
    
    // 阻塞线程,等待 group 中的操作(不包含 dispatch_group_notify)都执行完后解除阻塞
    // dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@" waiting... ");
    NSLog(@" waiting... ");
    NSLog(@" waiting... ");
}



dispatch_semaphore 相关的3个函数

1
2
3
4
5
6
7
8
9
dispatch_time 的声明如下:

// 以下方法返回值表示 when 向后延时 delta(单位是纳秒)时间
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta)

例如:

// timeout 表示当前时间向后延时一秒。
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000);


1
2
3
4
5
6
7
8
9
10
// 创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_t dispatch_semaphore_create(long value);

// 接收一个信号和时间值(一般为 DISPATCH_TIME_FOREVER )
// 若信号量等于 0,则会阻塞当前线程,直到信号量大于 0 或者等待时间 timeout 后程序才会继续往下执行
// 若信号量大于 0,则会使信号量减 1 并返回,程序继续住下执行
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

// 提高信号量, 使信号量加 1 并返回
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 为线程加锁
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
for (int i = 0; i < 20; i++) {
    dispatch_async(queue, ^{
        // 相当于加锁,若执行这句代码之前的信号量大于 0,则这句代码返回值是 0;否则返回值不为 0
        long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"i = %d semaphore = %@", i, semaphore);

        // 相当于解锁,某些时候少了这句代码会报错:EXC_BAD_INSTRUCTION
        dispatch_semaphore_signal(semaphore);
    });
}



dispatch_group_enter 和 dispatch_group_leave

当 group 中的 异步并发 任务内部又嵌套了 异步并发 任务时,内部嵌套的异步并发任务在没有执行完时就会开始执行 dispatch_group_notify。这时可以用以上两个方法解决,真正达到时使 dispatch_group_notify 中的任务在 group 中最后执行

注意

  • dispatch_group_enter :未完成的任务计数 + 1,往 group 中添加一个任务
  • dispatch_group_leave :未完成的任务计数 - 1,从 group 中移除一个任务
  • 只要未完成的任务计数 > 0,dispatch_group_notify 就不会执行
  • 两者必须成对出现,配合使用,否则会崩溃。


示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQ = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);

// 多个 dispatch_group_enter 可以都写在开头
dispatch_group_enter(group);
//    dispatch_group_enter(group);
//    dispatch_group_enter(group);
dispatch_group_async(group, concurrentQ, ^{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"1--over");
        dispatch_group_leave(group);
    });
});
    
dispatch_group_enter(group);
dispatch_group_async(group, concurrentQ, ^{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:5];
        NSLog(@"2--over");
        dispatch_group_leave(group);
    });
});
    
dispatch_group_enter(group);
dispatch_group_async(group, concurrentQ, ^{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3--over");
        dispatch_group_leave(group);
    });
});

dispatch_group_notify(group, concurrentQ, ^{
    NSLog(@"其他全部结束");
});
    
NSLog(@" waiting... ");
NSLog(@" waiting... ");
NSLog(@" waiting... ");



dispatch_suspend & dispatch_resume

  • 队列挂起和恢复,甚至可以在不同的线程对这个队列进行挂起和恢复,因为 GCD 是对队列的管理
  • 对已经在执行中的任务没有影响,队列中尚未执行的停止执行
  • 恢复指定的 queue 队列,使尚未执行的任务继续执行
  • 当队列没有挂起时调用 dispatch_resume 方法会崩溃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    for (int i = 0; i < 100; i++) {
        NSLog(@"%i",i);
        if (i == 50) {
            NSLog(@"-----------------------------------");
            dispatch_suspend(queue);
            sleep(3);
            dispatch_async(dispatch_get_main_queue(), ^{
                dispatch_resume(queue);
            });
        }
    }
});



Dispatch Source 调度源

  • Dispatch Source 能通过合并事件的方式确保在高负载下正常工作
  • DISPATCH_SOURCE_TYPE_DATA_ADD 在很短的时间间隔内,一个事件的的触发频率很高,那么 Dispatch Source 会将这些响应以 ADD 的方式进行累积,然后等系统空闲时一次性处理,如果触发频率比较零散,那么 Dispatch Source 会将这些事件分别响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 创建 source,以 ADD 的方式进行累加,而 DISPATCH_SOURCE_TYPE_DATA_OR 是对结果进行二进制或运算
// 此处定义的句柄在主线程执行
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    
__block NSUInteger totalComplete = 0;
    
// 事件触发后执行的句柄
dispatch_source_set_event_handler(source, ^{
    // 这个数据的值在每次响应事件执行后会被重置,所以 totalComplete 的值是最终累积的值。
    NSUInteger value = dispatch_source_get_data(source);
    totalComplete += value;
    NSLog(@"待处理的数据量:%lu", dispatch_source_get_data(source));
    NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
    
    dispatch_source_cancel(source);
});
    
// 源取消时的回调
dispatch_source_set_cancel_handler(source, ^{
    NSLog(@"## 如果source 被取消,则执行这个函数 ##");
});

// 调用 dispatch_resume(source) 方法恢复调度源后,就会执行这个 block,
// 并不需要调用 dispatch_source_merge_data(source, 1) 才执行
dispatch_source_set_registration_handler(source, ^{
    NSLog(@"我是注册收到的回调");
});

// source 创建完默认是挂起的,需要手动调用方法恢复。
dispatch_resume(source);

// 如果调用完 dispatch_resume(source); 后再直接调用 dispatch_source_merge_data(source, 1); 
// 则可能会不触发事件 block,莫名其妙? 但是只要在 block 中调用一次 dispatch_source_get_data(source); 就会正常。。。
// 所以最好在 dispatch_source_set_event_handler 的 block 中调用一次 dispatch_source_get_data(source) 
// dispatch_source_merge_data(source, 1);

// 简单来个串行队列
dispatch_queue_t queue = dispatch_queue_create(nil, 0);
    
// 恢复源后,就可以通过 dispatch_source_merge_data 向调度源发送事件
for (NSUInteger index = 0; index < 100; index++) {
    dispatch_async(queue, ^{
        // 可在任意线程触发事件,这里 i 必须大于 0,否则触发不了事件
        dispatch_source_merge_data(source, 1);
        
        NSLog(@"线程号:%@~~~~~~~~~~~~i = %ld", [NSThread currentThread], index);
        
        // 睡眠 0.02秒,当间隔的时间足够长,则每次都会触发句柄
        usleep(20000);
    });
}

// 这里仅仅是暂停 source 去调用前面设置的句柄,而 for 循环并不会暂停
// dispatch_suspend(source);

// 要想真正暂停 source,需要让 Dispatch Source 与 Dispatch Queue 同时实现暂停和恢复
// dispatch_suspend(queue);



使用 dispatch_source_set_timer 定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
when:从什么时间开始,一般直接传 DISPATCH_TIME_NOW 表示从现在开始
delta:单位是纳秒! 表示具体的时间长度,可以写成这种形式: 3 * NSEC_PER_SEC
dispatch_time(dispatch_time_t when, int64_t delta)
    
    
timespec 是一个结构体, 创建的是一个绝对的时间点,比如 2016101083030秒。
如果传 NUll 表示自动获取当前时区的当前时间作为开始时刻
dispatch_walltime(const struct timespec *_Nullable when, int64_t delta);
    
举例,当前时间往后 5 秒:
dispatch_time_t time = dispatch_walltime(NULL, 5 * NSEC_PER_SEC);
    
以上两个函数的区别:
第一个函数,如果设备进入休眠,系统时钟也休眠,则 dispatch_time 函数也会休眠;设备唤醒后函数也同时被唤醒
第二个函数则不受系统时钟是否休眠的影响,都能准备计时。


source   调度源
start    定时器开始时间。需要用 dispatch_time  dispatch_walltime 函数来创建。
interval 定时器的间隔时间
leeway   定时器触发的精准程度
dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, uint64_t interval, uint64_t leeway)




// =======================================================================




// 倒计时时间
__block int timeout = 3;
    
// 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
// 创建 timer
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
// 设置 1s 触发一次,0s 的误差
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0*NSEC_PER_SEC, 0);
    
// 触发的事件
dispatch_source_set_event_handler(_timer, ^{
    if (timeout <= 0) {
        // 取消dispatch源
        dispatch_source_cancel(_timer);
    } else {
        timeout--;
        dispatch_async(dispatch_get_main_queue(), ^{
            // 更新主界面的操作
            NSLog(@"~~~~~~~~~~~~~~~~%d", timeout);
        });
    }
});
    
// 恢复定时器调度源。如果调度源类型是 DISPATCH_SOURCE_TYPE_TIMER,则恢复后会自动触发句柄事件
dispatch_resume(_timer);

本文由作者按照 CC BY 4.0 进行授权