多线程技术,首选的是NSOperation
,在有些场合,考虑直接用GCD
。c语言的风格,block
的调用方式,更加灵活可控。
由于苹果推荐说用GCD
,不过这是c风格了,跟Object-C这种面向对象的风格不是很符合。还是推荐NSOperation
为主。当然,方便的时候,用用简单的经典的GCD
也无妨。AFNetworking就是将NSOperation
和GCD
结合起来用的,比较经典。
GCD
中一些类型的定义在系统的如下路径
/usr/include/dispatch/
下面这两篇文章比较好地解释了同步异步,串行并行的概念,可以参考一下。虽然文章名字差不多,不过真的是不一样的,而且写得都还不错。
iOS多线程与GCD 你看我就够了
关于iOS多线程,你看我就够了
同步/异步(执行)
差异点在于是否阻塞当前的调用者线程,等待队列中的任务执行完毕。
同步执行
typedef void (^dispatch_block_t)(void);
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
- “阻塞”当前的调用者线程
- “等待”队列中的任务执行完毕
- 激活当前的调用者线程,“然后”继续往下执行其他代码
- 如果队列中的任务长时间不返回,会出现界面“卡死”现象
- 如果调用者线程和队列执行线程是“同一线程”,会出现“自己等自己”现象,结果是“崩溃或者死机”
比如下面的代码如果在主线程执行,就会“崩溃或者死机”,原因就是出现了“主线程等主线程自己的情况”
NSLog(@"之前 - %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"sync - %@", [NSThread currentThread]);
});
NSLog(@"之后 - %@", [NSThread currentThread]);
异步执行
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
格式基本和同步的差不多,就一个单词的差别,但是表现完全两样。上面的例子用这个函数的话,就不会“崩溃或者死机”。其输出一般情况是这样的:
之前 -
之后 -
sync -
- 只是派发队列中的函数,让其去执行,不阻塞自己,不等待
- 派发完后,直接执行下面的语句,碰到
}
,自己就先返回 - 队列中的函数如何执行,在哪执行,当前的调用者线程完全不关心
- 异步调用可以使“当前的调用者”和“队列如何执行”毫无关系,充分隔离
- 如果当前做的事和队列中要做的事没有先后依赖关系,那么推荐用异步调用的方式
串行/并行(队列)
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
第一个参数是队列的名字,是个字符串;
第二个参数决定队列的性质,串行或者并行;
虽然有ARC,但生成的Dispatch Queue
必须由程序员主动释放。
dispatch_release(exampleSerialDispatchQueue); // 释放
dispatch_retain(exampleSerialDispatchQueue); // 持有
- 两者的本质区别是:后面的任务是否等待前面的任务执行完毕
串行队列
- 第二个参数是
DISPATCH_QUEUE_SERIAL
或者是"NULL" - 任务FIFO;因为只有一个管道,所以任务之间间有顺序依赖。就算是异步执行的,也要等前面的任务执行完毕,后面的任务才能执行
- 队列的优先级较高,串行队列中的任务,有先后顺序依赖,后面的任务要等待
- 如果任务间有顺序依赖,可以用串行队列,不需要考虑进程间同步的问题。“线程安全的”就是指串行队列。比如,可以把字典、数组操作放入一个串行队列,就解决“资源竞争”的问题了。
- 只开辟一个线程执行,一个执行完了,再接着下一个,所以没有时序问题,是“线程安全的”
- 主线程是一种串行队列,有时候主线程“卡死”,就是因为它在等待“耗时的任务”执行完毕
ispatch_queue_t dispatch_get_main_queue(void) {
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);
}
并行队列
- 第二个参数是
DISPATCH_QUEUE_CONCURRENT
- 任务也是FIFO。但是后面的任务不需要等待前面的任务执行结束,没有先后的依赖关系。
- 后面的任务要后出队列,调度顺序上要落后一点。但是由于任务时间的关系。后面的任务完全有可能比前面的任务的先执行完毕
- 并行队列不需要等待前面的任务执行完毕,效率上会高一点。如果任务间没有顺序上的依赖,推荐用并行队列
- 如果是异步执行,可以开辟多个线程,任务间时序是不确定的。当然,如果是同步执行,任务间还是得等待,有先后依赖关系。
- 系统提供的默认可用的并行队列
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
#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_PRIORITY_DEFAULT
;第2个参数基本上是0
单例
+ (instancetype)sharedManager {
static AFNetworkReachabilityManager *_sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_len = sizeof(address);
address.sin_family = AF_INET;
_sharedManager = [self managerForAddress:&address];
});
return _sharedManager;
}
- 这是AFNetworking中一个例子,单例基本就这个格式,非常方便
-
static
可以是全局的,也可以是局部的。这里是局部的,其他文件访问不到,更好一点 -
typedef long dispatch_once_t;
本质是个long
,这里有特殊含义,特殊用途
延时执行
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
typedef uint64_t dispatch_time_t;
#define DISPATCH_TIME_NOW (0ull)
#define DISPATCH_TIME_FOREVER (~0ull)
上面就是常用的函数和相应的配套函数和参数。第1个参数是时间,一般用第2个函数来获得,名字看上去都差不多。最小单位是纳秒,所以一般时间要用到下面几个常数定义。这个延时比NSTimer要精确一点,很多第3方库都在用,可以作为经典形式。
下面就是YYCache中的一个实际的例子
- (void)_trimRecursively {
__weak typeof(self) _self = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
_autoTrimInterval
就是要延迟执行的秒数
Group操作
有时候我们会有这种需求,在刚进去一个页面需要发送两个请求,并且更新页面操作必须在两个请求都结束(成功或失败)的时候才会执行。
第1种方式是用串行队列,两个网络请求依次进行,然后更新界面。这样做时序没有问题,不过当这两个网络请求之间没有相互依赖的话,效率损失比较大。
第2种方式是用NSOperationQueue
,将最后的更新页面操作依赖前面2个网络操作就可以了,其他的事交给系统。这种是推荐的方式
第3种是用GCD
的dispatch_group
函数族,可以获得更高的性能,更灵活地控制。这种方式不推荐,不过在AFNetworking
中有比较大量的应用,所以也不反对使用。
dispatch_group_t dispatch_group_create(void);
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
void dispatch_group_leave(dispatch_group_t group);
dispatch_group_wait
比较特殊,正常情况下不需要用到。比如,在这个例子中,两个网络请求之后就更新界面了。但是如果网络请求因为某种原因长时间不返回呢(这种情况还是蛮多的)?那就设一个超时时间吧。比如,最多等30s,如果还不返回,就显示网络不给力了。这个函数是同步的,傻等,结果看返回值,0表示成功。常见的超时等待处理方式。这种能力NSOperationQueue
是没有的。
代码参考格式:
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//在这里执行异步请求A
并且在执行结束代码(成功或失败)中写上dispatch_group_leave(group);
});
dispatch_async(dispath_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//在这里执行异步请求B
并且在执行结束代码(成功或失败)中写上dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//在这里执行异步请求B
});
比NSOperationQueue
复杂不了多少。
关于超时的参考格式:
#define kTimeOut 30ull
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (kTimeOut * NESC_PER_SEC));
long result = dispatch_group_wait(group, time);
if(0 == result) {
// 在限定时间内返回;dispatch_group_notify执行,正常情况;啥也不用做,当然也可以log一下
} else {
// 超时了,任务还没有全部完成,dispatch_group_notify这时还没有执行
// 如果这里显示超时,网络请求并不能取消,这个有点尴尬,dispatch_group_notify还是有可能执行的,只是观众不愿意等了
};
一些扫尾的工作
在文件object.h
中有一些函数定义:
void dispatch_suspend(dispatch_object_t object);
void dispatch_resume(dispatch_object_t object);
void dispatch_cancel(void *object);
void dispatch_release(dispatch_object_t object);
- 在上面超时的例子中,是不是可以把
group
直接dispatch_cancel
掉? - 在离开页面的时候,是不是要把
group
dispatch_release
? - 有传说
CGD
的任务不能cancel
,那是因为block
是匿名函数,没法获得地址。如果把block
用个变量保存起来,不是可以cancel
了吗?或者直接cancel
或者suspend
block
所在的queue
,不是能取消了吗?
栅栏函数(这部分内容有点多,Option)
// 异步栅栏,比较常用
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
// 同步栅栏,会阻塞当前的调用者线程,
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
栅栏函数可以看做是dispatch_group
的升级版。就像一个分界点,将队列一分为二,前后两部分有顺序依赖。可以简单的认为:
- 执行栅栏函数之前加入队列的任务;等待,直到任务全部执行完毕
- 执行栅栏函数添加到队列中的任务;
- 等待栅栏函数添加的任务,直到任务全部执行完毕。
- 执行栅栏函数之后添加的任务
下面这篇文章写得很不错,值得好好看看。以前一直讨厌这个栅栏,这次算有点理解了,开始喜欢这个栅栏函数:
通过GCD中的dispatch_barrier_(a)sync加强对sync中所谓等待的理解
实际的例子
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
dispatch_barrier_async(_queue, ^{
[_array insertObject:anObject atIndex:index];
});
}
- 这是
weex
框架中的一个例子 - 在类
WXThreadSafeMutableArray
中 - 这里可以当做
dispatch_group
的简化使用,因为栅栏之后没内容,作用跟dispatch_group
一样,不过使用简单很多,是一种很讨巧的方式 - 鼓励这种用法,相比而言,
dispatch_group
实在有点复杂
自己写的一个例子
- 任务用简单的
sleep
表示 - 先加入的任务时间长(1秒),后加入的任务时间短(0.5秒),在同步的情况下是顺序的,在异步的情况下是倒序的,容易区分
- 调用者线程就是主线程,好理解一点
case1: 异步栅栏在前,同步栅栏在后
这种情况,将有可能影响当前线程(主线程)的同步操作放在后面,对当前线程的影响最小,相对好理解一点。
代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
dispatch_queue_t currentQueue = dispatch_queue_create("currentQueue", DISPATCH_QUEUE_CONCURRENT);
// 异步执行
dispatch_async(currentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_async 1");
});
dispatch_async(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_async 2");
});
// 栅栏异步执行
dispatch_barrier_async(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_barrier_async");
});
NSLog(@"main thread after dispatch_barrier_async");
// 栅栏同步执行
dispatch_barrier_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_barrier_sync");
});
NSLog(@"main thread after dispatch_barrier_sync");
// 同步执行
dispatch_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_sync 3");
});
dispatch_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_sync 4");
});
}
输出的log:
2017-03-09 17:09:25.573 Barrier[82252:5039848] main thread after dispatch_barrier_async
2017-03-09 17:09:26.141 Barrier[82252:5039953] dispatch_async 2
2017-03-09 17:09:26.639 Barrier[82252:5039954] dispatch_async 1
2017-03-09 17:09:26.639 Barrier[82252:5039954] dispatch_barrier_async
2017-03-09 17:09:26.640 Barrier[82252:5039848] dispatch_barrier_sync
2017-03-09 17:09:26.640 Barrier[82252:5039848] main thread after dispatch_barrier_sync
2017-03-09 17:09:27.715 Barrier[82252:5039848] dispatch_sync 3
2017-03-09 17:09:28.289 Barrier[82252:5039848] dispatch_sync 4
过程分析:
-
main thread
执行到dispatch_barrier_sync
, 被阻塞,等待。这个时候,main thread after dispatch_barrier_async
已经输出 - 任务1和2异步执行,逆序输出(2的任务时间短)
- 异步栅栏执行,输出
dispatch_barrier_async
- 同步栅栏执行,输出
dispatch_barrier_sync
-
main thread
被唤醒,继续执行,输出main thread after dispatch_barrier_sync
-
main thread
执行到dispatch_sync 3
, 被阻塞,等待。 - 任务3,4同步执行,顺序输出(4的任务时间短也没用,要等3完成才能执行)
-
main thread
被唤醒,继续执行,碰到}
返回,过程结束
case2: 同步栅栏在前,异步栅栏在后
这里主要看栅栏同步异步的影响
代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
dispatch_queue_t currentQueue = dispatch_queue_create("currentQueue", DISPATCH_QUEUE_CONCURRENT);
// 异步执行
dispatch_async(currentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_async 1");
});
dispatch_async(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_async 2");
});
// 栅栏同步执行
dispatch_barrier_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_barrier_sync");
});
NSLog(@"main thread after dispatch_barrier_sync");
// 栅栏异步执行
dispatch_barrier_async(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_barrier_async");
});
NSLog(@"main thread after dispatch_barrier_async");
// 同步执行
dispatch_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_sync 3");
});
dispatch_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_sync 4");
});
}
输出的log:
2017-03-09 20:59:47.526 Barrier[91896:5120619] dispatch_async 2
2017-03-09 20:59:48.027 Barrier[91896:5120621] dispatch_async 1
2017-03-09 20:59:48.528 Barrier[91896:5120438] dispatch_barrier_sync
2017-03-09 20:59:48.528 Barrier[91896:5120438] main thread after dispatch_barrier_sync
2017-03-09 20:59:48.529 Barrier[91896:5120438] main thread after dispatch_barrier_async
2017-03-09 20:59:49.029 Barrier[91896:5120621] dispatch_barrier_async
2017-03-09 20:59:50.051 Barrier[91896:5120438] dispatch_sync 3
2017-03-09 20:59:50.626 Barrier[91896:5120438] dispatch_sync 4
过程分析:
-
main thread
执行到dispatch_barrier_sync
, 被阻塞,等待。这个时候,啥也没有输出 - 任务1和2异步执行,逆序输出(2的任务时间短)
- 同步栅栏执行,输出
dispatch_barrier_sync
-
main thread
执行到dispatch_sync 3
, 被阻塞,等待。这段时间main thread
做的事情有:(a)main thread after dispatch_barrier_sync
输出;(b)异步栅栏被分配,但是main thread
没有被阻塞;(c)main thread after dispatch_barrier_async
输出; - 异步栅栏执行,输出
dispatch_barrier_async
- 任务3,4同步执行,顺序输出(4的任务时间短也没用,要等3完成才能执行)
-
main thread
被唤醒,继续执行,碰到}
返回,过程结束
case3: 同步过程在前,异步过程在后
这个例子,调用者线程(这里是main thread
)分配任务之后先于工作者线程返回,是大多数要用到的情况。
代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
dispatch_queue_t currentQueue = dispatch_queue_create("currentQueue", DISPATCH_QUEUE_CONCURRENT);
// 同步执行
dispatch_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_sync 3");
});
dispatch_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_sync 4");
});
// 栅栏同步执行
dispatch_barrier_sync(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_barrier_sync");
});
NSLog(@"main thread after dispatch_barrier_sync");
// 栅栏异步执行
dispatch_barrier_async(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_barrier_async");
});
NSLog(@"main thread after dispatch_barrier_async");
// 异步执行
dispatch_async(currentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_async 1");
});
dispatch_async(currentQueue, ^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"dispatch_async 2");
});
}
输出的log:
2017-03-09 21:08:39.715 Barrier[92752:5127802] dispatch_sync 3
2017-03-09 21:08:40.285 Barrier[92752:5127802] dispatch_sync 4
2017-03-09 21:08:40.856 Barrier[92752:5127802] dispatch_barrier_sync
2017-03-09 21:08:40.856 Barrier[92752:5127802] main thread after dispatch_barrier_sync
2017-03-09 21:08:40.856 Barrier[92752:5127802] main thread after dispatch_barrier_async
2017-03-09 21:08:41.430 Barrier[92752:5127902] dispatch_barrier_async
2017-03-09 21:08:41.999 Barrier[92752:5127901] dispatch_async 2
2017-03-09 21:08:42.499 Barrier[92752:5127902] dispatch_async 1
过程分析:
- 任务3,4同步执行,顺序输出(4的任务时间短也没用,要等3完成才能执行)
- 同步栅栏执行,输出
dispatch_barrier_sync
- 这段时间
main thread
一直被阻塞,直到同步栅栏执行完毕 -
main thread
被唤醒,继续执行,碰到}
返回,main thread
的过程结束。这段时间main thread
做的事情有:(a)main thread after dispatch_barrier_sync
输出;(b)异步栅栏被分配,但是main thread
没有被阻塞;(c)main thread after dispatch_barrier_async
输出;(d)任务1和2被分配,但是main thread
没有被阻塞; - 异步栅栏执行,输出
dispatch_barrier_async
- 任务1和2异步执行,逆序输出(2的任务时间短)
小结
- 同步过程会阻塞当前的调用者线程,等待block中的任务执行完毕。除非必要,同步过程尽量不要用
- 栅栏函数可以作为
dispatch_group
一种简化用法,推荐使用。用异步栅栏就可以了。同步栅栏没有必要,增加理解的难度。 - 串行队列可以用在“线程安全的字典”等场景,比较方便
- 单例和延迟执行很经典,推荐使用
- 能用
dispatch_group
和栅栏函数解决问题的地方就不要用mutex,信号量等进程间同步的技术,一不小心就死锁了 - 栅栏比
NSOperationQueue
的依赖还好用,循环依赖也会带来死锁问题的。不过还是建议使用NSOperationQueue
,上层API
在内存管理等方面更让人省心和放心
网友评论