iOS多线程:GCD (三)
什么是CGD呢?以下摘自苹果的官方说明。
Grand Central Dispatch (GCD) 是异步执行任务的技术之一。应用程序中记述的线程管理用的代码是在系统级中实现的。开发者只需要定义想要执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并执行任务。
也就是说,GCD用我们难以置信的非常简洁的方法,实现了极为复杂的多线程编程。本文将罗列GCD的API及其用法实例来帮助大家了解GCD。
1、 GCD的队列和任务
GCD中有两个核心的概念:任务
和队列
1.1 任务 (Dispatch Block)
任务: 就是你想让系统执行的操作,GCD中通常是放在dispatch_block_t中的代码。任务分为同步执行(sync)和异步执行(async)两种执行方式。
-
同步执行(sync)
: 任务被同步添加到指定的队列中,在该任务执行结束前会一直等待。不具备开启线程的能力,只能在当前线程中同步执行任务。 -
异步执行(async)
: 任务被异步添加到指定队列中,不会等待该任务执行。具备开启线程的能力,可在新线程中执行任务。
注: 异步执行虽然具有开启新线程的能力,但只有该任务追加到并发队列才会开启新线程。
1.2 队列 (Dispatch Queue)
队列 (Dispatch Queue) : 是执行任务的的等待队列。开发者通过 dispatch_async
或dispatch_sync
函数等API,在dispatch_block_t
中记述想要执行的任务并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序(先进先出FIFO,First-In-First-Out)执行任务。队列分为 串行队列(Serial Dispatch Queue) 和 并发队列(Concurrent Dispatch Queue) 。
-
串行队列(Serial Dispatch Queue) : 只开启一条新的线程,追加到该队列中的任务会依次按顺序执行。
-
并发队列(Concurrent Dispatch Queue) : 会开辟多条新的线程,追加到该队列中的任务会并行执行。
注:
- 并发队列虽然有开启多条新线程的能力,但是只有在异步执行任务时才会开启新线程。
- 并发队列开启的新线程个数并不等同于任务个数,取决于队列的任务数、CPU核数、以及CPU负荷等当前系统状态。
2、GCD任务的创建、队列的获取和创建
2.1 创建同步及异步任务
dispatch_async(queue, ^{
//创建异步任务
});
dispatch_sync(queue, ^{
//创建同步任务
});
或者这样创建:
dispatch_block_t block = ^{
//任务
};
dispatch_async(queue, block); //异步执行
dispatch_sync(queue, block); //同步执行
2.2 获取系统主队列
主队列为特殊的串行队列,只有一个线程,即为主线程。主线程用于界面UI更新、用户事件交互等操作。所以比较耗时的操作(如查询数据库,数据请求等)都不应放在主线程中执行,会造成页面卡顿,影响用户体验。
dispatch_queue_t queue = dispatch_get_main_queue();
2.3 获取全局并发队列
系统为我们提供了四个全局的并发队列,我们可以直接获取,用来执行任务。
CPU执行任务处理时按照队列的优先级来分配资源,决定任务执行的先后顺序。但是苹果通过XUN内核用于Global Dispatch Queue 的线程并不能保证实时性 (Why? I don’t know.),因此执行优先级只是大致的判断。
//第一个参数为队列优先级,第二个参数没用到,传0就行。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
全局并发队列的优先级:
DISPATCH_QUEUE_PRIORITY_HIGH //高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT //默认优先级
DISPATCH_QUEUE_PRIORITY_LOW //低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND //后台级
2.4 创建 串行队列(Serial Dispatch Queue)
//1.第一个参数为队列名称,推荐使用工程ID这种逆序命名方式,便于管理和调试。
//2.第二个参数传NULL或DISPATCH_QUEUE_SERIAL,表示串行队列。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
2.5 创建 并发队列(Concurrent Dispatch Queue)
//1.第一个参数为队列名称,推荐使用工程ID这种逆序命名方式,便于管理和调试。
//2.第二个参数传DISPATCH_QUEUE_CONCURRENT,表示并发队列。
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd",DISPATCH_QUEUE_CONCURRENT);
3、 不同队列及任务的不同执行方式对比
由上我们可以看出:要想并发执行某些任务,只有使用 〖并发队列 + 异步执行〗这种组合方式。这也是我们开发中最常用的组合方式。
3.1 串行队列 + 同步执行
- (void)serialAndSync {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block1 = ^{
sleep(2);
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block2 = ^{
sleep(1);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block3 = ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
};
dispatch_sync(queue, block1);
dispatch_sync(queue, block2);
dispatch_sync(queue, block3);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
输出结果:
begin ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
1 ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
2 ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
3 ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
结论:
1.所有任务都在主线程中执行(【串行+同步】不会开启新线程)。
2.所有任务按照追加顺序依次执行。
3.任务执行在begin和end之间,同步执行任务会阻塞主线程。
3.2 串行队列 + 异步执行
- (void)serialAndAsync {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block1 = ^{
sleep(2);
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block2 = ^{
sleep(1);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block3 = ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
};
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_async(queue, block3);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
输出结果:
begin ==== >>: <NSThread: 0x1d0078800>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d0078800>{number = 1, name = main}
1 ==== >>: <NSThread: 0x1c0073200>{number = 3, name = (null)}
2 ==== >>: <NSThread: 0x1c0073200>{number = 3, name = (null)}
3 ==== >>: <NSThread: 0x1c0073200>{number = 3, name = (null)}
结论:
1.【串行+异步】只开启一条新线程,所有任务按照追加顺序依次执行。
2.任务执行在end之后,异步执行任务不会阻塞主线程。
3.3 并发队列 + 同步执行
- (void)concurrentAndSync {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_block_t block1 = ^{
sleep(2);
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block2 = ^{
sleep(1);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block3 = ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
};
dispatch_sync(queue, block1);
dispatch_sync(queue, block2);
dispatch_sync(queue, block3);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
输出结果:
begin ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
1 ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
2 ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
3 ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d4074f80>{number = 1, name = main}
结论:
1.所有任务都在主线程中执行(同步执行不会开启新线程)。
2.所有任务按照追加顺序依次执行。
3.任务执行在begin和end之间,同步执行任务会阻塞主线程。
3.4 并发队列 + 异步执行
- (void)concurrentAndAsync {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_block_t block1 = ^{
sleep(2);
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block2 = ^{
sleep(1);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block3 = ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
};
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_async(queue, block3);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
输出结果:
begin ==== >>: <NSThread: 0x1d0263e00>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d0263e00>{number = 1, name = main}
3 ==== >>: <NSThread: 0x1d4269fc0>{number = 3, name = (null)}
2 ==== >>: <NSThread: 0x1c007f140>{number = 4, name = (null)}
1 ==== >>: <NSThread: 0x1c807e740>{number = 5, name = (null)}
结论:
1.任务执行在end之后 (不在主线程中执行,开启多条新线程执行)。不会阻塞主线程
2.所有任务并发执行,不会等待。
3.5 主队列 + 同步执行
- (void)mainAndSync {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_block_t block1 = ^{
sleep(2);
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block2 = ^{
sleep(1);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block3 = ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
};
dispatch_sync(queue, block1);
dispatch_sync(queue, block2);
dispatch_sync(queue, block3);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
输出结果:
begin ==== >>: <NSThread: 0x1d0263e00>{number = 1, name = main}
结论:
发生死锁,任务不会执行。由于主队列为串行队列,主线程在执行mainAndSync 函数,而 mainAndSync 在等待主线程执行结束,就造成了互相等待,均不会执行。(开发中要极力避免这种情况)
3.6 主队列 + 异步执行
- (void)mainAndAsync {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_block_t block1 = ^{
sleep(2);
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block2 = ^{
sleep(1);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
};
dispatch_block_t block3 = ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
};
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_async(queue, block3);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
输出结果:
begin ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
1 ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
2 ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
3 ==== >>: <NSThread: 0x1d4076040>{number = 1, name = main}
结论:
1.由于主队列为串行队列(不会开启新线程),所有任务都在主线程中执行。
2.任务在end之后执行,主队列中开启异步任务,不会开启新线程,会降低异步任务的优先级,在CPU空闲时才会执行该任务。
4. GCD中的 dispatch_set_target_queue
的用法及作用
4.1 更改Dispatch Queue 的执行优先级
通过 dispatch_queue_create
函数生成的GCD队列,不管是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue,其优先级都为系统默认优先级,若想改变创建队列的优先级,则可以使用 dispatch_set_target_queue
函数。
// 第一个参数为需要更改优先级的queue, 第二个参数为参照队列,
// 将参照队列的优先级设为目标队列的优先级
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd",DISPATCH_QUEUE_SERIAL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue(queue, globalQueue);
4.2 设置多个 Dispatch Queue 的层级
由上我们知道,通过 dispatch_queue_create
函数创建的 Serial Dispatch Queue 任务执行是同步的,同时只能执行一个任务,虽然GCD队列受到系统资源的限制,但通过 dispatch_queue_create
函数可以生成任意多个 Dispatch Queue。
当生成多个 Serial Dispatch Queue时,各个 Serial Dispatch Queue将并行执行。
如下代码:
- (void)setTargetQueue {
dispatch_queue_t queue1 = dispatch_queue_create("com.example.gcd.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.queue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("com.example.gcd.queue3", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
sleep(3);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
});
dispatch_async(queue2, ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
sleep(2);
NSLog(@"4 ==== >>: %@",[NSThread currentThread]);
});
dispatch_async(queue3, ^{
NSLog(@"5 ==== >>: %@",[NSThread currentThread]);
sleep(1);
NSLog(@"6 ==== >>: %@",[NSThread currentThread]);
});
}
执行结果:
3 ==== >>: <NSThread: 0x1d0066180>{number = 5, name = (null)}
5 ==== >>: <NSThread: 0x1d4468e80>{number = 4, name = (null)}
1 ==== >>: <NSThread: 0x1d0066700>{number = 3, name = (null)}
6 ==== >>: <NSThread: 0x1d4468e80>{number = 4, name = (null)}
4 ==== >>: <NSThread: 0x1d0066180>{number = 5, name = (null)}
2 ==== >>: <NSThread: 0x1d0066700>{number = 3, name = (null)}
多次运行发现: 其中1、3、5的输出顺序不固定,多个任务并发执行。
假如此时我想让多个同步队列中的任务还是依次同步执行,或者让多个并发队列中的任务同步执行,我该怎么办呢?对,使用 dispatch_set_target_queue
,将三个队列指定到同一串行目标队列上,此时多个队列任务就会同步执行,不再是并发执行了。
如下代码:
- (void)setTargetQueue {
dispatch_queue_t queue1 = dispatch_queue_create("com.example.gcd.queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.gcd.queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue3 = dispatch_queue_create("com.example.gcd.queue3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t targetQueue = dispatch_queue_create("com.example.gcd.targetQueue", DISPATCH_QUEUE_SERIAL);
//指定到同一串行队列
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"1 ==== >>: %@",[NSThread currentThread]);
sleep(3);
NSLog(@"2 ==== >>: %@",[NSThread currentThread]);
});
dispatch_async(queue2, ^{
NSLog(@"3 ==== >>: %@",[NSThread currentThread]);
sleep(2);
NSLog(@"4 ==== >>: %@",[NSThread currentThread]);
});
dispatch_async(queue3, ^{
NSLog(@"5 ==== >>: %@",[NSThread currentThread]);
sleep(1);
NSLog(@"6 ==== >>: %@",[NSThread currentThread]);
});
}
执行结果:
1 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
2 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
3 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
4 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
5 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
6 ==== >>: <NSThread: 0x1d02602c0>{number = 3, name = (null)}
在必须将不可并行执行的多个任务追加到多个 Serial Dispatch Queue 中时,可使用 dispatch_set_target_queue函数,防止任务并发执行。
注: 一旦生成 Serial Dispatch Queue 并追加任务处理,系统对于一个 Serial Dispatch Queue 就会生成一条线程。假如生成 1000个 Serial Dispatch Queue ,那么就生成了 1000 条线程。 此时会大量消耗内存,大幅度降低系统的响应性能。
5. GCD时间: dispatch_time_t
dispatch_time_t
为GCD的时间类,用于获取距离某个目标时间间隔的时间
计算相对时间:
dispatch_time_t time = dispatch_time(<#dispatch_time_t when#>, <#int64_t delta#>)
//第一个参数为某个目标时间,常用 DISPATCH_TIME_NOW,表示现在时间。
//第二个参数表示具体的时间长度,不能直接传 int或者float,需 (int64_t)3* NSEC_PER_SEC 这种形式。
注: delta单位是纳秒!
NSEC_PER_SEC 1000000000ull 每秒有1000000000纳秒
NSEC_PER_MSEC 1000000ull 每毫秒有1000000纳秒
USEC_PER_SEC 1000000ull 每秒有1000000微秒
NSEC_PER_USEC 1000ull 每微秒有1000纳秒
"ull"是C语言的数值字面量,是显示表明类型时使用的字符串(表示“unsigned long long”)。
例如 dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC) 表示距离当前时间 3秒后的时间。
计算绝对时间:
dispatch_time_t time =dispatch_walltime(<#const struct timespec * _Nullable when#>, <#int64_t delta#>)
/* 第一个参数是一个 timespec 的结构体, 计算的是一个绝对的时间点,比如 2016年10月10日8点30分30秒,
如果你不需要自某一个特定的时刻开始,可以传 NUll,表示自动获取当前时区的当前时间作为开始时刻,
第二个参数同上。*/
例如 dispatch_walltime(NULL, 3*NSEC_PER_SEC)表示距离当前时间 3秒后的时间。
struct timespec 类型的时间可以通过NSDate对象生成:
// 通过指定日期获取一个 dispatch_time_t 对象
- (dispatch_time_t)getDispatchTimeByDate:(NSDate *)date {
NSTimeInterval interval;
double second, subsecond;
struct timespec time;
dispatch_time_t milestone = 0;
interval = [date timeIntervalSince1970];
subsecond = modf(interval, &second);
time.tv_sec = second;
time.tv_nsec = dispatch_walltime(&time, 0);
return milestone;
}
6. GCD延时操作: dispatch_after
我们可能经常会有这样的需求,想在3秒后执行某项操作,可能不限于3秒,总之,这种想在指定时间后执行操作的情况,可用 dispatch_after
来实现。
例如:
//第一个参数是 dispatch_time_t 的值(详见5)。
//第二个参数是执行的 Dispatch Queue 。
//第三个参数是要追加的操作。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"Three seconds later...");
});
注: dispatch_after 函数并不是在指定的时间过后执行操作,而只是在指定时间后追加操作到 Dispatch Queue 中。
因为 Mian Dispatch Queue 在主线程的RunLoop 中执行,假如主线程的刷新频率为 60 帧,则追加的操作最快在3秒后执行,最慢则在 3 + 1/60 秒后执行,并且假如在Mian Dispatch Queue 中有大量操作要执行或者主线程本身有延迟的话,这个时间会更长。
7. Dispatch Group
7.1 dispatch_group_notify
假如在追加到 Dispatch Queue 中的多个操作全部结束后想要执行某项操作,假如只使用一个 Serial Dispatch Queue时,只需将结束操作追加到所有任务的最后即可实现。但是在使用 Concurrent Dispatch Queue或同时使用多个 Dispatch Queue时,实现起来就比较复杂了。
这时我们就可以使用 Dispatch Group 。下面我们来看一段代码:
- (void)dispatchGroup {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
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(@"Task 1 ==== >>: %@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"Task 2 ==== >>: %@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"Task 3 ==== >>: %@",[NSThread currentThread]);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"Done! ==== >>: %@",[NSThread currentThread]);
});
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
执行结果:
begin ==== >>: <NSThread: 0x1d006f940>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d006f940>{number = 1, name = main}
Task 2 ==== >>: <NSThread: 0x1d0473700>{number = 3, name = (null)}
Task 3 ==== >>: <NSThread: 0x1c806cd00>{number = 4, name = (null)}
Task 1 ==== >>: <NSThread: 0x1c406c740>{number = 5, name = (null)}
Done! ==== >>: <NSThread: 0x1c806cd00>{number = 4, name = (null)}
因为Global Dispatch Queue 为 Concurrent Dispatch Queue ,多个线程并发执行任务,所以任务执行顺序不定,但是执行结果 Done 一定是最后执行的。无论使用的是什么类型的 Dispatch Queue,Dispatch Group 都可以监测到这些任务执行的结束。
通过
dispatch_group_create()
函数生成 Dispatch Group .
dispatch_group_async
与dispatch_async
相同,都是追加 Block操作到指定 Dispatch Queue中。不同的是dispatch_group_async
多了一个 Group 参数。
7.2 dispatch_group_wait
如上情况我们也可以使用 dispatch_group_wait
函数去实现,如下代码:
- (void)dispatchGroup {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
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(3);
NSLog(@"Task 1 ==== >>: %@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"Task 2 ==== >>: %@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"Task 3 ==== >>: %@",[NSThread currentThread]);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"Done! ==== >>: %@",[NSThread currentThread]);
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
执行结果:
begin ==== >>: <NSThread: 0x1d4262fc0>{number = 1, name = main}
Task 3 ==== >>: <NSThread: 0x1d026f040>{number = 3, name = (null)}
Task 2 ==== >>: <NSThread: 0x1c007ff40>{number = 4, name = (null)}
Task 1 ==== >>: <NSThread: 0x1cc07fe80>{number = 5, name = (null)}
Done! ==== >>: <NSThread: 0x1d4262fc0>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d4262fc0>{number = 1, name = main}
从结果我们可以看出
dispatch_group_wait
一样可以达到我们想要的结果。但是不同之处也非常明显,dispatch_group_wait
会阻塞当前线程。
另外 dispatch_group_wait
函数可以指定等待的时间,传入 DISPATCH_TIME_FOREVER 则表示永远等待,直到所有任务执行结束。且该函数还有 long 型的返回值,返回0则表示所有任务都执行结束。
如我们修改代码如下:
long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC));
if (result == 0) {
//所有任务都执行结束
NSLog(@"Done! ==== >>: %@",[NSThread currentThread]);
}else {
//有任务尚未结束
NSLog(@"Not Done! ==== >>: %@",[NSThread currentThread]);
}
执行结果为:
begin ==== >>: <NSThread: 0x1d0264f40>{number = 1, name = main}
Task 3 ==== >>: <NSThread: 0x1d066e980>{number = 3, name = (null)}
Task 2 ==== >>: <NSThread: 0x1cc07f9c0>{number = 4, name = (null)}
Not Done! ==== >>: <NSThread: 0x1d0264f40>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d0264f40>{number = 1, name = main}
Task 1 ==== >>: <NSThread: 0x1c807fc80>{number = 5, name = (null)}
由上我们可以看出 dispatch_group_notify
与 dispatch_group_wait
两个函数的异同之处。在开发过程中可根据具体情况选择使用。
7.3 dispatch_group_enter
和 dispatch_group_leave
这两个函数配对使用,表示 Dispatch Group 开启任务和结束任务。这两种通知可以在多线程间自由穿梭,不局限于特定的某个线程。
dispatch_group_enter
: 通知group,下面的任务马上要放到group中执行了。
dispatch_group_leave
: 通知group,任务完成了,该任务要从group中移除了。
猜测: 可能 Dispatch Group 内部有着一个类似引用计数的存在,调用 dispatch_group_enter
会加一,调用 dispatch_group_leave
会减一,当该值为0时,则调用 dispatch_group_notify
和 dispatch_group_wait
函数。
当我们异步开启一个任务,并不指定特定的队列及线程时,可使用这两个函数去通知 Dispatch Group 任务的开启和结束。
如下代码:
- (void)dispatchGroup {
NSLog(@"begin ==== >>: %@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
[self downloadImaegWithUrl:@"http://pic10.photophoto.cn/20090224/0036036802407491_b.jpg" InGroup:group];
[self downloadImaegWithUrl:@"http://pic25.photophoto.cn/20121220/0036036800277861_b.jpg" InGroup:group];
[self downloadImaegWithUrl:@"http://img.taopic.com/uploads/allimg/140806/235020-140P60H10661.jpg" InGroup:group];
dispatch_group_notify(group, queue, ^{
NSLog(@"Done! ==== >>: %@",[NSThread currentThread]);
});
NSLog(@"end ==== >>: %@",[NSThread currentThread]);
}
//利用 SDWebImage异步下载图片
- (void)downloadImaegWithUrl:(NSString *)url InGroup:(dispatch_group_t)group{
dispatch_group_enter(group);//开启任务
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:url] options:(SDWebImageDownloaderContinueInBackground) progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
NSLog(@"下载完成! ====>>: %@",[NSThread currentThread]);
dispatch_group_leave(group);//结束任务
}];
}
执行结果:
begin ==== >>: <NSThread: 0x1d406ee00>{number = 1, name = main}
end ==== >>: <NSThread: 0x1d406ee00>{number = 1, name = main}
下载完成! ====>>: <NSThread: 0x1d406ee00>{number = 1, name = main}
下载完成! ====>>: <NSThread: 0x1d406ee00>{number = 1, name = main}
下载完成! ====>>: <NSThread: 0x1d406ee00>{number = 1, name = main}
Done! ==== >>: <NSThread: 0x1cc069cc0>{number = 3, name = (null)}
8. GCD栅栏 (dispatch_barrier_sync)
当我们在访问数据库或者文件时,使用 Serial Dispatch Queue 可避免数据竞争导致程序异常的问题(多个写入操作不可并行执行)。但是这样的话,程序的执行效率就比较低。
为了高效率的进行访问,我们使用 Concurrent Dispatch Queue,并使用 dispatch_barrier_sync
函数解决数据竞争问题。
dispatch_barrier_sync
俗称栅栏,顾名思义,即可以将某些操作隔绝开来,互不影响。
假如我想在4次读取操作中间加入2次写入操作, 如下代码:
- (void)dispatchBarrier {
dispatch_queue_t queue = dispatch_queue_create("com.gcd.example", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"reading1 ===>>: %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"reading2 ===>>: %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
//写入操作
NSLog(@"writing1 ===>>: %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
//写入操作
NSLog(@"writing2 ===>>: %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"reading3 ===>>: %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"reading4 ===>>: %@",[NSThread currentThread]);
});
}
由 Concurrent Dispatch Queue 的性质我们可知,4次读取和2次写入均会并发执行,先后顺序不定,这样必然会导致数据竞争,出现异常。
修改代码: 我们只需将写入操作替换成 dispatch_barrier_sync
函数即可解决该问题。
dispatch_barrier_sync(queue, ^{
//写入操作
NSLog(@"writing1 ===>>: %@",[NSThread currentThread]);
});
dispatch_barrier_sync(queue, ^{
//写入操作
NSLog(@"writing2 ===>>: %@",[NSThread currentThread]);
});
dispatch_barrier_sync
函数会等待追加到 Dispatch Queue 上的并行任务全部执行结束之后,再将指定的操作追加到 Dispatch Queue 中。该指定的操作执行完毕后,Dispatch Queue 才会恢复正常操作。开始处理其他任务。
因此修改后的代码执行顺序为 reading1和reading2并发执行在前,之后为 writing1 和 writing2 串行执行,最后 reading3和reading4并发执行。
9. GCD信号量 (dispatch_semaphore_t)
信号量(dispatch_semaphore_t)可以理解为有信号通过,无信号则等待。具体的我们慢慢来分析。
与信号量有关的主要有三个函数:
dispatch_semaphore_create(<#long value#>)
//创建一个信号量,传入一个long型的信号值.
//传入信号值为大于或者等于0的值,否则将返回 NULL.
dispatch_semaphore_wait(<#dispatch_semaphore_t _Nonnull dsema#>, <#dispatch_time_t timeout#>)
//与 dispatch_group_wait(详见7.2)类似,会阻塞当前线程,并有返回值.
//当当前信号量等于0时,持续等待.
//当当前信号量大于0时,执行返回,并将当前信号量减一.
dispatch_semaphore_signal(<#dispatch_semaphore_t _Nonnull dsema#>)
//该函数会将当前信号量加一.
我理解的信号量主要有三个作用:线程同步、线程锁、控制并发数.
9.1 线程同步
先看代码:
__block int count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
count = 10;
});
NSLog(@"%d",count);
由异步并发执行我们可知,此时输出的count值为 0。假如我想让主线程保持与任务异步线程一致,输出 count 值为10,我该怎么做呢?
修改代码:
dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
__block int count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
count = 10;
dispatch_semaphore_signal(semphore);
});
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
NSLog(@"%d",count);
分析:
我们初始化了一个值为0信号量,执行 dispatch_semaphore_wait 函数时发现信号量为0,则会等待信号,执行完 count = 10 后,通过 dispatch_semaphore_signal 函数获得一个信号量,此时 dispatch_semaphore_wait 监测到信号量大于0,则执行返回,线程继续执行,输出 count 为10。这就保证了线程间的同步了。
9.2 线程锁
假如我想异步并发执行1000次任务,给同一块内存区间赋值,会发生什么情况呢?
先看代码:
NSMutableArray *temArr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[temArr addObject:[NSString stringWithFormat:@"%d",i]];
});
}
运行会发现程序异常崩溃的概率相当高,这是因为我们异步并发的操作同一内存导致数据错乱引起的异常。
使用线程锁修改代码:
dispatch_semaphore_t semphore = dispatch_semaphore_create(1);
NSMutableArray *temArr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//等待信号
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
//到这里,信号量会减一
[temArr addObject:[NSString stringWithFormat:@"%d",i]];
dispatch_semaphore_signal(semphore);
//信号量加一
});
}
通过这种方法,保证了同一时间只有一个操作去改变内存。当然在实际操作中我们不会这样去for循环使用,通常是设置一个全局的 dispatch_semaphore_t 对象,将需要加锁的操作放在,dispatch_semaphore_wait 和 dispatch_semaphore_signal 函数中间。保证唯一性。
例如:
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
self.semaphore = dispatch_semaphore_create(1);
- (void)setName:(NSString *)name {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
_name = name;
dispatch_semaphore_signal(self.semaphore);
}
9.3 控制并发数
当我们在处理大量线程时,怎么来进行并发控制呢,假如我有1000个任务要执行,但是同时执行的并发数我想控制在10个,该怎么做呢?以前我们使用NSOperationQueue可以控制并发数,在GCD中怎么简单快速的控制并发呢?那就是使用 dispatch_semaphore_t.
先看代码:
- (void)dispatchSemaphore {
dispatch_group_t group = dispatch_group_create();
dispatch_semaphore_t semphore = dispatch_semaphore_create(10);
for (int i = 0; i < 100; i++) {
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"%d ===>>: %@",i,[NSThread currentThread]);
sleep(2);
dispatch_semaphore_signal(semphore);
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"end");
}
首先我们创建了一个值为10的信号量,每次循环会减少一个信号量,执行结束会增加一个信号量,当执行操作为10个时,此时信号量为0,dispatch_semaphore_wait 函数将会等待,如此则保证了最多同时有10个任务在执行。由此看出,通过 dispatch_semaphore_t 来控制并发数简单快速又实用。
10. dispatch_suspend / dispatch_resume
当追加大量的处理任务到 Dispatch Queue 时,有时候希望不执行某些已经追加的任务,在这种情况下,只需要挂起 Dispatch Queue即可,当可以执行时再恢复。
dispatch_suspend
: 挂起指定的 Dispatch Queue。可理解为暂停执行。dispatch_resume
: 恢复指定的 Dispatch Queue。可理解为继续执行。
值得注意的是,dispatch_suspend 对于正在执行的任务是没有影响的,挂起后,只有 Dispatch Queue 中尚未执行的任务停止执行。而恢复则使得这些任务能够继续执行。
11. GCD快速遍历 (dispatch_apply)
dispatch_apply
函数是dispatch_sync函数和 Dispatch Group的关联API。该函数按指定次数将指定的 Block 追加到指定的 Dispatch Queue中,并等待全部处理执行结束。
如下代码:
- (void)dispatchApply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu",index);
});
NSLog(@"end");
}
执行结果:
0
2
4
3
5
6
7
8
9
1
end
因为在 Global Dispatch Queue 中执行操作,10个任务并发执行,但是最后的end一定在最后的位置上。因为 dispatch_apply 会等待全部任务执行结束。
另外,由于dispatch_apply 函数与 dispatch_sync函数一样,会等待任务结束,阻塞当前线程,因此推荐在 dispatch_async 函数中异步执行 dispatch_apply 函数。
例如:
- (void)dispatchApply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//异步执行
dispatch_async(queue, ^{
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu",index);
});
//等待处理结束
dispatch_async(dispatch_get_main_queue(), ^{
//主线程回调,刷新UI
NSLog(@"end");
});
});
NSLog(@"main");
}
12. GCD单例 (dispatch_once)
dispatch_once
函数是保证在应用程序执行中只执行一次指定处理的API。
下面这种经常出现的用来进行初始化的代码可通过 dispatch_once
函数来简化。
static int count = 0;
if (count == 0) {
//在这里初始化
count = 100;
}
使用 dispatch_once 函数:
static int count = 0;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//在这里初始化
count = 100;
});
代码看起来并没有太大的变化。但是通过 dispatch_once
函数,该代码即使在复杂的多线程环境下执行,也可保证百分之百安全。之前的代码在大多数情况下也是安全的。但是在多核CPU中,读取数据时,有可能会多次执行初始化处理。而用 dispatch_once
函数就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。