NSMutableArray并发场景下的内存越界问题

最近使用ObjC和Swift混合编程,发现ios的ARC策略在并发下还是存在很大的问题。我的看法是,如果涉及到并发编程,最好还是不要过于依赖ARC机制。

以下是问题:

我们在一个需要处理实时数据的App里面,考虑采用NSMutableArray实现一个简易的FIFO队列。逻辑是这样的:

@implementation UTKSimpleQueue
@synthesize capacity = _capacity;

- (instancetype) initWithCapacity:(int)capacity {
    if (self = [super init]) {
        queue = [[NSMutableArray alloc] init];
        _capacity = capacity;
        availability = 0;
    }
    return self;
}

- (BOOL) push:(NSObject*)obj {
    BOOL result = YES;
    [lck lock];
    if (availability >= _capacity) {
        result = NO;
    }
    else {
        [queue addObject: obj];
        availability ++;
    }
    [lck unlock];
    return result;
}

- (NSObject*) pop {
    NSObject *obj = nil;
    [lck lock];
    if (availability > 0) {
        availability --;
        obj = queue[availability];
        [queue removeObjectAtIndex:availability];
    }
    [lck unlock];
    
    return obj;
}

- (void) flush {
    [lck lock];
    [queue removeAllObjects];
    availability = 0;
    [lck unlock];
}

- (NSObject*) peek {
    NSObject *obj = nil;
    [lck lock];
    if (availability > 0) {
       obj = queue[availability];
    }
    [lck unlock];
    return obj;
}

@end

严格来说,这是一个blocking队列,采用了NSLock实现数据的线程安全。在其他的语言下,一个FIFO blocking队列是最容易实现的结构。但是我们没有想到的是,NSMutableArray在push与pop操作时,会频繁遇到NSZombie和线程的不同步问题。例如在两个线程里,采用这样的代码

UTKSimpleQueue *inputQueue;
UTKSimpleQueue *outputQueue;

//thread 1:
NSObject *obj;
[inputQueue push:obj];
...
//thread 2:
NSObject obj = [inputQueue pop];
[outputQueue push:obj];

就会发生message send to dealloc object的报错,核查之后发现inputQueue当中存储了NSZombie_...的对象。

这个问题是偶发的,随机的。在痛苦的尝试了一周的调试之后,最终,我决定放弃利用NSMutableArray,采用最直接的双向链表实现了一个线程安全的BlockingQueue:

@implementation UTKBlockingQueue

- (instancetype) initWithCapacity:(int)capacity {
    if (self = [super init]) {
        _capacity = capacity;
        _availability = 0;
        lock = [[NSLock alloc] init];
        first = nil;
        last = first;
    }
    return self;
}

- (BOOL) push:(NSObject*)obj {
    BOOL result = true;

    [lock lock];
    if (_availability >= _capacity) {
        result = NO;
    }
    else {
        UTKBlockingQueueItem *item = [[UTKBlockingQueueItem alloc] init];
        _availability ++;
        if (!first) {
            item.prev = nil;
            item.next = nil;
            item.obj = obj;
            first = item;
        }
        else {
            item.prev = last;
            item.next = nil;
            item.obj = obj;
        }
        last.next = item;
        last = item;
        result = YES;
    }
    [lock unlock];
    
    return result;
}

- (NSObject*) pop {
    if (!first) {
        return nil;
    }
    else {
        UTKBlockingQueueItem *item = first;
        
        [lock lock];
        first = first.next;
        first.prev = nil;
        _availability --;
        [lock unlock];
        
        NSObject *obj = item.obj;
        item.next = nil;
        item.obj = nil;
        return obj;
    }
}

- (NSObject*) peak {
    return first.obj;
}

- (void) flush {
    [lock lock];
    while(first && first.next) {
        UTKBlockingQueueItem *item = first;
        first = item.next;
        first.prev = nil;
        
        item.next = nil;
        item.prev = nil;
        item.obj = nil;
        
        item = first;
    }
    first.prev = nil;
    first.obj = nil;
    last = first;
    last = nil;
    _availability = 0;
    [lock unlock];
}

- (int) remain {
    return _availability;
}
@end

@implementation UTKBlockingQueueItem
@synthesize obj;
@synthesize prev;
@synthesize next;

- (instancetype) init {
    if (self = [super init]){
        self.obj = nil;
        self.prev = nil;
        self.next = nil;
    }
    return self;
}
@end

因此,在ObjC涉及到多线程并发操作和需要动态申请内存的应用场景下,我的个人建议是:

  1. malloc操作之务必进行bzero操作
  2. 高频队列操作不要选择NSArray和NSMutableArray
  3. 所有的指针释放之后手动设置为NULL

你可能感兴趣的:(NSMutableArray并发场景下的内存越界问题)