偶遇野指针导致数组 crash 了

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

  1. 全局锁,最傻的办法,给整个 Array 加互斥锁,用 NSLock 或 @synchronized 来实现,实现简单但并发效率低,任何读写操作都保证由一个线程序获取资源,阻塞其他线程,保证串行化执行。
- (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 就无法识别是否发生了变化,这里可以想下如何解决这个问题。