41、多用派发队列,少用同步锁
OC中,如果有多个线程执行同一份代码,有时可能会出问题。通常情况下,使用锁来实现某种同步机制。
GCD之前有两种方法
- 1、内置的同步块(synchronization block)
- (void)synchronizeMethod {
@synchronized(self) {
//
}
}
根据给定对象,自动创建一个锁,并等待块中农代码执行完毕。执行到折断代码结尾处,锁就释放了。
优点:同步行为针对self,保证每个对象实例都能不受干扰地运行方法synchronizeMethod
。
缺点:滥用会降低代码效率,共用同一个锁的那些同步块,都必须按顺序执行。若是self对象上频繁加锁,程序可能要等另一端无关的代码执行完毕,才能执行当前代码。
- 2、NSLock/NSRecursiveLock
_lock = [[NSLock alloc] init];
- (void)synchronizeMethod {
[_lock lock];
//
[_lock unlock];
}
也可以使用NSRecursiveLock
,线程能够多次持有该锁,不会出现死锁(deadlock)现象。
两种方法都很好,也有缺陷。比方说,在极端情况下,同步块会导致死锁,另外效率也不见得很高,而如果直接使用锁对象的话,遇到死锁,就很麻烦。
GCD实现
- 1、串行同步队列(serial synchronization queue)
将读取操作及写入操作都安排在同一个队列里,保证数据同步。
_syncQueue = dispatch_queue_create("com.effectiveOC.syncQueue", NULL);
- (NSString *)someString {
__block NSString *localString;
//为使块代码能够设置局部变量,使用__block语法。
dispatch_sync(_syncQueue, ^{
localString = self.someString;
});
return localString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
self.someString = someString;
});
}
把设置操作和获取操作都安排在序列化的队列里执行,这样的话,所有针对属性的访问操作都同步了。
全部加锁任务都在GCD中处理。
继续优化,设置方法并不一定非得同步,设置实例变量所用的块,并不需要向设置方法返回什么值。
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
self.someString = someString;
});
}
同步派发改成异步派发,可以提升设置方法的执行速度,而读取操作与写入操作依然会按顺序执行。
执行异步派发时,需要拷贝块。如果拷贝所用的时间明显超过执行块所用的时间,则这种方法比原来慢。但是,若是派发给队列的块要执行更为繁重的任务,那么仍然可以考虑这种备选方案。
并发队列(concurrent queue)
多个获取方法可以并发执行,但获取方法与设置方法之间不能并发执行。
栅栏(barrier),在队列中,栅栏必须单独执行,不能与其它块并行。下面方法可以像对立中派发块,将其作为栅栏使用。
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync(dispatch_queue_t queue,
DISPATCH_NOESCAPE dispatch_block_t block);
只对并发队列有意义,因为串联队列中的块总是安顺序逐个执行的,并发队列如果发现接下来要处理的是栅栏块,那么就一直要等当前的所有并发块都执行完毕,才会单独执行这个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。
例子中,可以用栅栏块来实现属性的设置方法,在设置方法中使用了栅栏块之后,对属性的读取操作依然可以并发执行,但是写入操作就必须单独执行了。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localString;
dispatch_sync(_syncQueue, ^{
localString = self.someString;
});
return localString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
self.someString = someString;
});
}
设置函数也可以改用同步的栅栏块(synchronous barrier)来实现。测试性能之后,选择最适合当前场景的方案。
- 派发队列可用来表述同步语义(synchronization semantic),这种做法比使用@synchronized或NSLock对象更简单。
- 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
- 使用同步队列栅栏,可以令同步队列更加高效。
42、多用GCD,少用performSelector系列方法
OC是一门非常动态的语言,NSObject定义了几个方法,开发者可以随意调用任意方法。
performSelector系列方法
- (id)performSelector:(SEL)aSelector;
如果选择子是在运行期决定的,这种方式就很强大。
SEL selector;
if (index == 2) {
selector = @selector(newObject);
} else if (index == 1) {
selector = @selector(copy);
} else {
selector = @selector(someProperty);
}
id ret = [objct performSelector:selector];
有两个问题:
- 1、ARC下可能会有内存泄露问题。编译器不知道将要调用的选择子是什么,不了解其方法签名及返回值,设置不知道是否有返回值。所以,ARC选择不添加释放操作,就可能导致内存泄露,因为方法可能在返回对象时已经将其保留了。
- 2、返回值只能是void或对象类型。performSelector返回的类型是id,指向任意的OC对象指针。如果想返回一些整数或浮点数等类型的值,就需要执行一些复杂的转换,而这种转换容易出错。若返回值是C语言结构体,则不可使用performSelector方法。
其他可传参数版本:
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
传参类型是id,另外选择子最多只能接受两个参数。
可以延后执行选择子,或将其放在另一个线程执行。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
这些方法太过局限。如具备延后执行的方法无法处理两个参数的选择子。能够指定执行线程的方法,也不能传多个参数。
GCD实现相同功能
performSelector系列方法所提供的线程功能,都可以通过在大中枢派发机制中使用块来实现,延后执行可使用dispatch_after来实现,另一个线程执行任务则可通过dispatch_sync及dispatch_async来实现。
例如:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0*NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
[self doSomethingElse];
});
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomethingElse];
});
- performSelector系列方法在内存管理方面容易有疏失,他无法确定将要执行的选择子具体是什么,因而ARC编译器无法插入适当的内存管理方法。
- performSelector系列方法所能处理的选择子泰国局限,选择子的返回值类型及发送给方法的参数个数都收到限制。
- 如果想把任务放到另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。
43、掌握GCD及操作队列的使用时机
很少有其他技术能与GCD的同步机制相媲美,对于那些只需执行一次的代码来说,也是如此,使用GCD的dispatch_once最为方便。然而在执行后台任务时,GCD不一定是最佳方式。
还有一种技术叫做NSOperationQueue,操作队列(operation queue)。它虽然与GCD不同,却与之相关,可以把操作以及NSOperation子类的形式放在队列中,而这些操作也能够并发执行。
区别:GCD是纯C的API,而操作队列则是OC的对象。GCD中,任务用块来表示,而块是个轻量级数据结构,与之相反,操作时更为重量级的OC对象。
优点:
- 1、取消某个操作。使用操作队列,取消操作很容易。在运行任务之前,在NSOperation对象上调用cancel方法,设置对象内的标志位,表明此任务不需要执行。已经启动的任务无法取消。若是不通过操作队列,而是把块安排到GCD队列,就无法取消了。
- 2、指定操作间的依赖关系。一个操作可以依赖其他多个操作。能够指定操作之间的依赖关系,使特定的操作必须在另一个操作顺利执行完毕后方可执行。
- 3、通过键值观察机制监控NSOperation对象的属性。NSOperation许多属性都可以通过KVO来监听,如通过isCancelled属性判断任务是否已经取消,通过isFinished判断是否已经完成。如果想在某个任务变更状态时收到通知,或想要比用GCD更精细的方式控制所要执行的任务,KVO会很有用。
- 4、指定操作优先级。操作优先级表示此操作与队列中其他操作之间的优先关系。优先级高限制性,优先级低后执行。GCD没有直接实现此功能的办法。GCD有优先级,不过是针对整个队列来说,而不是针对每个块来说的。 NSOperation对象也有线程优先级,决定运行此操作的线程处于何种优先级上。GCD可以实现此功能,采用操作队列更简单,只需设置一个属性。
- 5、重用NSOperation对象。
操作队列提供了多种执行任务的方式,而且都是写好的,直接就能使用。不需要编写复杂的调度器,也不用自己实现取消操作或者指定操作优先级的功能。
NSNotificationCenter选用操作队列而非派发队列。可以通过其中的方法来注册监听器,一般发生相关事件时得到通知,这个方法接受的参数是块,不是选择子。
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
object:(nullable id)obj
queue:(nullable NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block;
尽可能使用高层API,只有在确有必要时才求助于底层。不过某些功能缺失可以使用高层OC的方法来做,但并不等于它就一定比底层实现方案好,具体看性能。
- 在解决多线程与任务管理问题时,派发队列并非唯一方案。
- 操作队列提供了一套高层OC API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD实现,需要另外编写代码。
44、通过Dispatch Group机制,根据系统资源状况来执行任务
dispatch group
是GCD的一项特性,能够把任务分组。这个功能有很多用途,最重要、最值得注意的用法就是把将要执行的多个任务合为一个组,于是调用者就可以知道这些任务何时才能执行完毕。
创建dispatch group
dispatch_group_t dispatch_group_create(void);
dispatch group就是个简单的数据结构,这种结构彼此之间没什么区别,它不像派发队列,后者还有个用来区分的标识符。
- 把任务编组方法:
void
dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
就是普通的dispatch_async
函数的辩题,比原来多一个参数,用于表示待执行的块所属的组。
- 指定任务所属的
dispatch group
void
dispatch_group_enter(dispatch_group_t group);
//使分组中正要执行的任务数递增
void
dispatch_group_leave(dispatch_group_t group);
//使分组中正要执行的任务数递减
调用dispatch_group_enter
必须有与之对应的dispatch_group_leave
才行。与引用计数相似,要使用引用计数,必须令保留操作与释放操作彼此对应,以防内存泄漏。
使用dispatch group
如果调用enter
之后,没有响应的leave
操作,这一组任务就永远执行不完。多调用leave
会崩溃。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//添加任务1
dispatch_group_enter(group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3*NSEC_PER_SEC)), queue, ^{
NSLog(@"11111");
dispatch_group_leave(group);
});
//添加任务2
dispatch_group_enter(group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(4*NSEC_PER_SEC)), queue, ^{
NSLog(@"2222");
dispatch_group_leave(group);
});
// 添加任务3
dispatch_group_enter(group);
NSLog(@"third");
dispatch_group_leave(group);
// 以不阻塞当前线程方式执行group
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"执行完所有任务:%@",[NSThread currentThread]);
});
NSLog(@"方法结束");
打印结果:
third
方法结束
11111
2222
执行完所有任务:<NSThread: 0x60800006fb40>{number = 1, name = main}
dispatch group执行函数
- 1、
dispatch_group_wait
--阻塞所在线程
long
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
此函数接受两个参数,第一个是要等待的group
,第二个是代表等待时间的timeout
值。timeout
表示函数等待dispatch group
执行完毕时,应该阻塞多久。如果执行dispatch group
所需的时间小于timeout
,则返回0,否则返回非0值。此参数可以取常量DISPATCH_TIME_FOREVER
,这表示函数会一直等着dispatch group
执行完毕,不会超时。
long num = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (3*NSEC_PER_SEC)));
NSLog(@"%ld",num);
- 2、
dispatch_group_notify
--不阻塞所在线程
void
dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
这个方法可以向此函数传入块,等待dispatch group
执行完毕之后,块会在特定的线程上执行。如果当前线程不应阻塞,又想在任务全部完成时得到通知,那么此做法就很有必要。第二个参数queue
即是想要回调的线程。
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"执行完所有任务:%@",[NSThread currentThread]);
});
示例
创建两个级别线程队列,分别创建任务添加到group,最后并发执行。
dispatch_group_t group = dispatch_group_create();
// 创建优先级低的线程队列
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
// 创建优先级高的线程队列
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_enter(group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), lowPriorityQueue, ^{
NSLog(@">>>任务1-low<<<");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), lowPriorityQueue, ^{
NSLog(@">>>任务2-low<<<");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), highPriorityQueue, ^{
NSLog(@">>>任务3-high<<<");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2*NSEC_PER_SEC)), highPriorityQueue, ^{
NSLog(@">>>任务4-high<<<");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"执行完所有任务:%@",[NSThread currentThread]);
});
NSLog(@"方法结束");
最后中间打印的顺序是不固定的,原因是,虽然设置了线程的优先级别,但是这个顺序是由系统决定的,并不保证首先执行。同时,这里的任务提交到并发队列,优先级问题效果不明显。
除了将任务提交到并发队列之外,还可以把任务提交到串行队列中。但是这种情况下,所有任务都排在同一个串行队列里,dispatch group
用处就不大了。因为此时任务总要逐个执行,秩序在提交完玩不任务之后再提交一个块即可。所以未必总需要使用dispatch group
,有时采用单个队列搭配标准的异步派发,也可以实现相同效果。
GCD有并发队列机制,所以能够根据可用的系统资源状况来并发执行任务。通过dispatch group
,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。
- 一系列任务可归入一个
dispatch group
中,开发者可以再这组任务执行完毕时获得通知。 - 通过
dispatch group
,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源装快来调度这些并发任务。
45、使用dispatch_once执行只需运行一次的线程安全代码
dispatch_once()
函数接受类型为dispatch_once_t
的特殊参数(标记token),此外还接受块参数。对于给定的标记来说,该函数保证相关的块必须执行,切仅执行一次。首次调用该函数时,必要会执行块中的代码,最重要的一点在于,此操作完全是线程安全的。对于只执行一次的块来说,每次调用函数时传入的标记都必须完全相同。因此,开发者通常将标记变量声明在static或global作用域里。
#import "ZYDUserManager.h"
@implementation ZYDUserManager
+ (id)sharedInstance {
static ZYDUserManager *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[ZYDUserManager alloc] init];
});
return sharedInstance;
}
@end
使用dispatch_once
可以简化代码并且彻底保证线程安全,无需但系加锁或同步。所有问题有GCD的底层实现。
另外dispatch_once
更高效,它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁,想法,此函数采用原子访问来查询标记,以判断其所对应的代码原来是否已经执行过。
- 经常需要编写只需要执行一次的安全代码(thread-safe single-code execution)。通过GCD提供的
dispatch_once
函数,很容易实现此功能。 - 标记应该声明在
static
或global
作用域中,这样,在把只需要执行一次的块传递给dispatch_once
函数时,传进去的标记也是相同的。
46、不要使用dispatch_get_current_queue
dispatch_queue_t dispatch_get_current_queue(void);
此方法已经被弃用。
-
dispatch_get_current_queue
函数的行为常常与开发者所预期的不同,此函数已经废弃,只应做调试之用。 - 由于派发队列是按成绩来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
-
dispatch_get_current_queue
函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用队列特定数据来解决。