最近使用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涉及到多线程并发操作和需要动态申请内存的应用场景下,我的个人建议是:
- malloc操作之务必进行bzero操作
- 高频队列操作不要选择NSArray和NSMutableArray
- 所有的指针释放之后手动设置为NULL
网友评论