程序员

编写高质量iOS与OSX代码的52个有效方法-第六章:大中枢派发

2018-07-25  本文已影响9人  竹与豆

41、多用派发队列,少用同步锁

OC中,如果有多个线程执行同一份代码,有时可能会出问题。通常情况下,使用锁来实现某种同步机制。

GCD之前有两种方法

- (void)synchronizeMethod {
    @synchronized(self) {
        //
    }
}

根据给定对象,自动创建一个锁,并等待块中农代码执行完毕。执行到折断代码结尾处,锁就释放了。

优点:同步行为针对self,保证每个对象实例都能不受干扰地运行方法synchronizeMethod

缺点:滥用会降低代码效率,共用同一个锁的那些同步块,都必须按顺序执行。若是self对象上频繁加锁,程序可能要等另一端无关的代码执行完毕,才能执行当前代码。

_lock = [[NSLock alloc] init];

- (void)synchronizeMethod {    
    [_lock lock];
    //
    [_lock unlock];
}

也可以使用NSRecursiveLock,线程能够多次持有该锁,不会出现死锁(deadlock)现象。

两种方法都很好,也有缺陷。比方说,在极端情况下,同步块会导致死锁,另外效率也不见得很高,而如果直接使用锁对象的话,遇到死锁,就很麻烦。

GCD实现

将读取操作及写入操作都安排在同一个队列里,保证数据同步。

_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)来实现。测试性能之后,选择最适合当前场景的方案。


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];

有两个问题:

其他可传参数版本:

- (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];
});

43、掌握GCD及操作队列的使用时机

很少有其他技术能与GCD的同步机制相媲美,对于那些只需执行一次的代码来说,也是如此,使用GCD的dispatch_once最为方便。然而在执行后台任务时,GCD不一定是最佳方式。

还有一种技术叫做NSOperationQueue,操作队列(operation queue)。它虽然与GCD不同,却与之相关,可以把操作以及NSOperation子类的形式放在队列中,而这些操作也能够并发执行。

区别:GCD是纯C的API,而操作队列则是OC的对象。GCD中,任务用块来表示,而块是个轻量级数据结构,与之相反,操作时更为重量级的OC对象。

优点:

操作队列提供了多种执行任务的方式,而且都是写好的,直接就能使用。不需要编写复杂的调度器,也不用自己实现取消操作或者指定操作优先级的功能。

NSNotificationCenter选用操作队列而非派发队列。可以通过其中的方法来注册监听器,一般发生相关事件时得到通知,这个方法接受的参数是块,不是选择子。

- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
                             object:(nullable id)obj
                              queue:(nullable NSOperationQueue *)queue
                         usingBlock:(void (^)(NSNotification *note))block;

尽可能使用高层API,只有在确有必要时才求助于底层。不过某些功能缺失可以使用高层OC的方法来做,但并不等于它就一定比底层实现方案好,具体看性能。


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函数的辩题,比原来多一个参数,用于表示待执行的块所属的组。

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执行函数

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);
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,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。


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更高效,它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁,想法,此函数采用原子访问来查询标记,以判断其所对应的代码原来是否已经执行过。


46、不要使用dispatch_get_current_queue

dispatch_queue_t dispatch_get_current_queue(void);此方法已经被弃用。


上一篇 下一篇

猜你喜欢

热点阅读