App 在 load 的时候任务过重影响了性能,所以很自然想做并发,我的场景是主线程对 Array 只做新增 append 操作,其他线程进行读 read 操作,只要 read 的 index 小于数组大小,自然认为不会 crash,但结果 exc_bad_access 了,写了2段测试code,fragment1 正常,fragment2 报错:
fragment 1: work well
- (void) testOneAddAndMultiReadBug
{
NSMutableArray *array = [NSMutableArray new];
for (NSInteger i=0; i<100000; i++) {
[array addObject:[VideoItem new]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger count = [array count];
VideoItem *e = array[count - 1];
});
[array addObject:[VideoItem new]];
}
}
fragment 2: not work
- (void) testOneAddAndMultiReadBug
{
NSMutableArray *array = [NSMutableArray new];
for (NSInteger i=0; i<100000; i++) {
[array addObject:[VideoItem new]];
array[[array count] - 1] = [VideoItem new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSInteger count = [array count];
VideoItem *e = array[count - 1]; //EXC_BAD_ADDRESS
});
[array addObject:[VideoItem new]];
}
}
CFArray 的 Append 实现如下:
struct __CFArrayImmutable {
CFRuntimeBase _base;
CFIndex _count; /* number of objects */
};
void CFArrayAppendValue(CFMutableArrayRef array, const void *value) {
_CFArrayReplaceValues(array, CFRangeMake(__CFArrayGetCount(array), 0), &value, 1);
}
void _CFArrayReplaceValues(CFMutableArrayRef array, CFRange range, const void **newValues, CFIndex newCount) {
const void **newv, *buffer[256];
cnt = __CFArrayGetCount(array);
futureCnt = cnt - range.length + newCount;
CFStorageRef store = ((struct __CFArrayMutable *)array)->_store;
if (range.length < newCount) {
CFStorageInsertValues(store, CFRangeMake(range.location + range.length, newCount - range.length));
}
if (0 < newCount) {
if (__kCFArrayMutableStore == __CFArrayGetType(array)) {
CFStorageRef store = ((struct __CFArrayMutable *)array)->_store;
CFStorageReplaceValues(store, CFRangeMake(range.location, newCount), newv);
}
}
__CFArraySetCount(array, futureCnt);
}
虽然append是先insert然后再replace,但setcount操作总在最后,所以 fragment 1没问题
但在 fragment 2 代码中,增了句 array[[array count] - 1] = [VideoItem new];
导致读线程读到了一个被释放的对象而 crash 了,在多线程操作下还是很容易造成异常。
如何解决
使用锁和非阻塞算法CAS
锁
- 全局锁,最傻的办法,给整个 Array 加互斥锁,用 NSLock 或 @synchronized 来实现,实现简单但并发效率低,任何读写操作都保证由一个线程序获取资源,阻塞其他线程,保证串行化执行。
- 读写锁,读线程之间不阻塞,只与写线程互斥,写线程之间相互互斥,这样来保证高效的并发读操作,fragment 2 改成:
- (void) testOneAddAndMultiReadBugFix
{
NSMutableArray *array = [NSMutableArray new];
__block pthread_rwlock_t rwlock;
if (pthread_rwlock_init(&rwlock, NULL)!=0) {
return; // error
}
for (NSInteger i=0; i<100000; i++) {
[array addObject:[VideoItem new]];
pthread_rwlock_wrlock(&rwlock);
array[[array count] - 1] = [VideoItem new];
pthread_rwlock_unlock(&rwlock);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_rwlock_rdlock(&rwlock);
NSInteger count = [array count];
VideoItem *e = array[count - 1];
pthread_rwlock_unlock(&rwlock);
});
}
pthread_rwlock_destroy(&rwlock);
}
3.局部锁,比如 Java 的 ConcurrentHashMap,DW上有几篇不错的分析文章,是JDK1.5后对原来synchronized table,Map的高并发替代方案,大致的思路把 Map 切成多个局部 Map,每个局部 Map 拥有锁,形成锁池,利用局部锁定来实现高并发的读写操作。 放个ConcurrentHashMap的put来事例下:
public V put(K key, V value) {
if (value == null)
throw new NullPointerException JavaDoc();
int hash = hash(key);
return segmentFor(hash).put(key, hash, value, false);
}
看到segmentFor(hash)应该就懂了;)Read more
非阻塞算法 Compare and Swap
CAS 比较有意思,CAS 是利用硬件指令轮询来检测内存值是否进行了变化,并进行赋值的原子操作,从而避免了使用锁,思想参见这里 举个例子:先读取某内存的值为A,然后执行CAS操作,CAS会再次读取这个内存的值并判断是否还是A,如果是A,则进行SWAP,替换成新值B,如果这之中被其他线程改变了值变成了C,则不赋值,重复再轮询,一直到SWAP成功为止。伪代码如下:
public final int getAndSet(int B) {
for (;;) {
int A = get();
if (compareAndSet(A, B))
return A;
}
}
OC提供的原子操作,见这里。
不过 CAS 有个 ABA 的 BUG,就是假如内存值一开始为 A,然后被其他线程更改为 B,然后再被更改回 A,这时候 CAS 就无法识别是否发生了变化,这里可以想下如何解决这个问题。