iOS并发编程Tips(一)

关于iOS并发编程,雷纯锋有篇博客做了很完整的介绍,大家可以移步学习一下。

我们在这里并不探究NSThreadGCDNSOperationNSOperationQueue的具体用法,只探讨一些容易被遗忘的小点。

线程成本

首先,什么是线程,维基百科上是这么说的:

A thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler.

按照雷纯锋的博客上的说法就是:

线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads。

在iOS中,进程启动之后,一个最主要的线程我们称为主线程。主线程会创建和管理所有的UI元素。一般来说,与用户交互相关的中断性操作都会派发到主线程上进行处理,包括你的IBAction的方法。

线程的创建是需要成本的,每个线程不仅仅在创建的过程中需要耗费时间,同时,它也会占用一定的内核的内存空间和app的内存空间。

Each thread has its own execution stack and is scheduled for runtime separately by the kernel. — Apple Thread Management

内核数据结构(Kernel data structures)

按照苹果官方文档上的说法,每个线程在内核空间上大概要消耗1KB大小的内存。而这块内存是用于存储线程的数据结构和属性的。这是一个连系内存(wired memory),不能在磁盘上分页。

This memory is used to store the thread data structures and attributes, much of which is allocated as wired memory and therefore cannot be paged to disk.

线程栈空间大小(Stack space)

在iOS中,主线程的栈空间大小为1MB, 在OS X中,主线程的栈空间大小为8MB,并且,这都是不可修改的。子线程默认栈空间为512KB

栈空间不是立即被创建分配的,它会随着使用而增长。所以说,即使主线程有1MB的栈空间,那么,在很大的一段时间之内,你都只会用到很少的一部分。

子线程允许分配的最小栈空间是16KB,并且,必须为4KB的倍数。我们可以通过stackSize属性来修改一个子线程的栈空间:

NSThread *t = [[NSThread alloc] initWithTarget:target
    selector:selector object:object];
t.stackSize = size;

线程创建时间(Creation time)

The figures were determined by analyzing the mean and median values generated during thread creation on an Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5.

按照苹果官方文档的说法,在一个2GHz的双核Intel处理器、1GB内存、OS X 10.5系统的iMac上,需要花费90微秒的时间(有些人会写90ms或者是90毫秒,其实,这里的ms是microsecond,而不是millisecond)。

原子属性

在声明属性的时候,我们两种选择,一种是atomic,一种是nonatomic,前者是原子的,后者是非原子的。基本上,他们的区别就在于,atomic会在属性的setter方法上加上一个互斥锁:自旋锁(spin lock)。

2016年4月14日纠错

按照objc.io的说法,这里应该是一个自旋锁(spin lock),在苹果开源的最新版runtime里面,依然是spin lock。具体位置为:objc-accessors.mm的93-97行):

spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;        
slotlock.unlock();

将例子中的@synchronized修改为OSSpinLock(spinlock_t使用起来没那么方便)。

atomic

OSSpinLock spinlock = OS_SPINLOCK_INIT;
- (void)setCurrentImage:(UIImage *)currentImage
{
    OSSpinLockLock(&spinlock);
    if (_currentImage != currentImage) {
        _currentImage = currentImage;
    }
    OSSpinLockUnlock(&spinlock);
}
- (UIImage *)currentImage
{
    OSSpinLockLock(&spinlock);
    id value = _currentImage;
    OSSpinLockUnlock(&spinlock);
    return value;
}

nonatomic

- (void)setCurrentImage:(UIImage *)currentImage
{
    if (_currentImage != currentImage) {
        _currentImage = currentImage;
    }
}
- (UIImage *)currentImage
{
    return _currentImage;
}

属性默认是atomic修饰的,明确写nonatomic才会是非原子操作。

比如:

@property(nonatomic, strong) UITextField *userName;
@property(atomic, strong) UITextField *userName;
@property(strong) UITextField *userName;

后两者其实是一样的,只有第一种才是非原子操作。

是否线程安全?

从上面的代码来看,atomic最多也就只能保证属性的settergetter方法是线程安全的。

我们举个例子,如果现在同时发生:

  • 线程A在调用getter方法。
  • 线程B、线程C在调用setter方法,并且它们设置的值是不一致的。

那么,线程A可能会获得原来的值,也可能会获得线程B或者线程C的值,这是不一定的。而且,属性最终的值可能是线程B,也可能是线程C设置的值。

用《Effective Objective-C 2.0》上面的话说,就是:

这么做虽然能提供某种程度的“线程安全”,但却无法保证访问该对象时绝对是线程安全的。当然,访问属性的操作确实是“原子”的。使用属性时,必定能从中获取到有效值,然而在同一个线程上多次调用获取方法,每次获取到的结果却未必相同。在两次访问操作之间,其他线程可能会写入新的值。

所以,要说到真正的线程安全,atomic的差距还是有点大的。

是否应该使用?

在没有资源竞争的情况下(比如,单线程的时候),atomic可能还是很快的,但是 在比较普遍的情况下,atomic想比起nonatomic可能会有靠近20倍的性能差异,stack overflow中有人对此进行了测试

那么,究竟是否该使用atomic呢,这个要看你是否需要。对我来说,我一般很少使用atomic,如果实在有需要的话,我一般会使用dispatch_barrier代替(具体例子可以参考下面的dispatch_barriersettergetter的写法)。

并发同步

在GCD上,我们有两种常见方法来让并发程序在某个点上进行同步,分别是dispatch_groupdispatch_barrier。相比起dispatch_barrier,我们可能用到dispatch_group的地方会更多一些。

dispatch_group允许向group中添加多个block块,在所有添加的block块全部执行完成之后,再通知其他队列执行其他的方法。而这个完成点就是并发的同步点。

dispatch_group的写法一般如下:

dispatch_queue_t dispatchQueue = dispatch_queue_create("com.ifujun.text", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
    NSLog(@"dispatch-1");
});
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
    NSLog(@"dspatch-2");
});
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
    NSLog(@"end");
});

dispatch_barrier就比较有意思了。dispatch_barrier是一个障碍点,在并发队列遇到dispatch_barrier之后,dispatch_barrier的block块会被延迟执行,直到所有在它之前提交的block块全部执行完成,然后才会开始执行dispatch_barrier的block块。

我们举个例子:

dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^(){
    NSLog(@"block 1");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"block 2");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"block 3");
});
dispatch_barrier_async(concurrentQueue, ^(){
    NSLog(@"barrier"); 
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"block 4");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"block 5");
});
dispatch_async(concurrentQueue, ^(){
    NSLog(@"block 6");
});

上面的代码中,block 1 - 6 都是可以并发执行的,但是由于barrier的存在,在block 1 - 3 执行完成之后,才会执行barrier,在barrier执行完成之后,才会并发执行剩下的block 4 - 6。

执行顺序如下图:

dispatch_barrier有一个比较常见的用法是读写锁。在上面的atomic上,我们说到,atomic因为给settergetter方法加锁,会造成很大的性能浪费,相当于同时只能一个线程在读或者写。

我们要的并不是单读单写,我们要的是多读单写,这样才能确保数据完整并且性能不错。

我们这以缓存举例(缓存必然需要有较高的性能,同时也要支持多读单写),如果用dispatch_barrier来实现的话,大概会是这样:

#import "FKCache.h"

@interface FKCache ()
@property (strong, nonatomic) NSMutableDictionary *cacheDictionary;
@property (strong, nonatomic) dispatch_queue_t queue;
@end

@implementation FKCache

+ (instancetype)shardInstance
{
    static FKCache *cache = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cache = [[FKCache alloc] init];
    });
    return cache;
}

- (instancetype)init
{
    if (self = [super init]) {
        _cacheDictionary = [NSMutableDictionary dictionary];
        _queue           = dispatch_queue_create("com.ifujun.readwritelock", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)setObjectForKey:(id)object forKey:(NSString *)key
{
    dispatch_barrier_async(self.queue, ^{
        [self.cacheDictionary setObject:object forKey:key];
    });
}

- (id)objectForKey:(NSString *)key
{
    __block id value = nil;
    dispatch_async(self.queue, ^{
        value = [self.cacheDictionary objectForKey:key];
    });
    return value;
}

@end

注意

dispatch_group比起来有一点很大的不同的是,dispatch_group上添加的block块可以来自于不同的并发队列,而dispatch_barrier只会阻塞同一个并发队列中的block。

参考文档

  1. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html
  2. http://blog.leichunfeng.com/blog/2015/07/29/ios-concurrency-programming-operation-queues/
  3. http://stackoverflow.com/questions/588866/whats-the-difference-between-the-atomic-and-nonatomic-attributes/589392#589392

Article Published in on iOS

Article by 付军