iOS 关于线程安全的Tips

atomic 不能绝对的保证线程安全

Set

这是 OC 中设置属性 与原子性有关情况所做的操作

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

我们可以看到,如果属性的修饰符是atomic 那么OC运行时会给我们的代码上自旋锁锁,
保证set方法的原子性。

Get

这是OC Get方法根据属性的原子性所做的操作

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;

    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();

atomic通过这种方法,在运行时保证 set,get方法的原子性。
但是! 仅仅是保证了set,get方法的原子性。

随便找个例子,这种线程就是不安全的。

@property (atomic, assign)    int       intA;

//thread A
for (int i = 0; i < 10000; i ++) {
    self.intA = self.intA + 1;
    NSLog(@"Thread A: %d\n", self.intA);
}

//thread B
for (int i = 0; i < 10000; i ++) {
    self.intA = self.intA + 1;
    NSLog(@"Thread B: %d\n", self.intA);
}

self.intA 是原子操作,但是self.intA = self.intA + 1这个表达式并不是原子操作。
所以线程是不安全的。
threadA 在执行表达式 self.intA之后 self.intA = self.intA + 1;并没有执行完毕
此时threadB 执行self.intA = self.intA + 1;
再回到threadA时,self.intA的数值就被更新了

线程安全

Notification

NSNotification 你需要注意的是,你在哪个线程上发送通知,就会在当前线程上调用selector.
所以还是在主线程发送通知吧。安全。

NSMutableArray, NSMutableDictionary

NSMutableArray, NSMutableDictionary,正如Apple文档所描述的,这两个collection不是线程安全的。
但是加锁会造成极大的性能问题。对于不是线程安全的容器类,应该用主线程去访问。
如果涉及到多线程,最好[xxx copy]或是使用不可变的容器类。

误解 Swift中的Immutable

用过Swift的人都知道,Swift相较于Objective-C有一个比较明显的改动就是将结构体(Struct)和类型(Class)进行了分离。从某种方面来说,Swift将值类型和引用类型进行了明显的区分。为什么要这么做?

  • 避免了引用类型在被作为参数传递后被他人持有后修改,从而引发比较难以排查的问题。
    在某些程度上提供了一定的线程安全(因为多线程本身的问题很大程序上出在写修改的不确定性)。而Immutable
  • 数据的好处在于一旦创建结束就无法修改,因此相当于任一一个线程在使用它的过程中仅仅是使用了读的功能。
    看到这,很多人开始欢呼了(嘲讽下WWDC那些“托”一般的粉丝,哈哈),觉得线程安全的问题迎刃而解了。

但事实上,我想说的是使用Immutable不直接等同于线程安全,不然在使用NSArray,NSDictionary等等Immutable对象之后,为啥还会有那么多奇怪的bug出现?

// Thread A 其中immutableArrayA count 7
self.xxx = self.immutableArrayA;

// Thread B 其中immutableArrayB count 4
self.xxx = self.immutableArrayB 

// main Thread
[self.xxx objectAtIndex:5]

锁能保证安全吗

OSSpinLock(&_lock);
[self.array addObject:@"hehe"];
OSSpinUnlock(&_lock);

原子锁只能解决Race Condition的问题,但是并不能解决代码执行时序的问题

比如如下这段代码:

if (self.xxx) {
    [self.dict setObject:@"ah" forKey:self.xxx];
}

ThreadA : 执行到if(self.xxx)结束
ThreadB : 执行self.xxx = nil
ThreadA : 执行[self.dict setObject:@”ah” forKey:self.xxx];就会崩溃了

针对这种情况有好的解决方案 那就是使用局部变量

__strong id val = self.xxx;
if (val) {
    [self.dict setObject:@"ah" forKey:val];
}

即使有其他线程尝试对self.xxx 修改, val指向的内存区域并没有被修改。

怎么实现一个”线程安全”的NSMutableArray

“相对线程安全” 由于NSMutableArray 是可变的,我们不能保证他的许多方法是线程安全的,我们可以进行重写通过加锁的方式或者设计一个新的线程安全容器类来保证线程安全。

一个线程安全类

@interface ThreadSafetyArray : NSObject {
@private
    NSMutableArray* _array;
}

- (void)addObject:(NSObject*)obj;
- (void)walk:(void (^)(NSObject*))walkfun;

@end

@implementation ThreadSafetyArray

- (id)init {
    self = [super init];
    if (self) {
        _array = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)addObject:(NSObject*)obj {
    @synchronized(self) {
        [_array addObject:obj];
    }
}

继承重写加锁:
查看Apple的文档,要继承这样的类需要必须实现其primitive methods方法,实现了这些方法,其它方法便都能通过这些方法组合而成。比如需要继承NSMutableArray就需要实现它的以下primitive methods:

- (void)addObject:(id)anObject;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;

和NSArray的primitive methods:

- (NSUInteger)count;
- (id)objectAtIndex:(NSUInteger)index;

而NSMutableDictionary也类似,在Apple文档中都有详细的描述。当然除此之外你都可以选择性的实现其它方法,以达到更高效率,比如NSMutableArray的removeAllObjects方法,因为默认他将循环调用removeLastObject方法达到目的,而你则可以选择更高效的实现方式。而要达到线程安全,不外乎就是在这些方法内部都加上锁,简化多线程情景下的容器使用,不必手动逐一添加锁。

有趣的事

for (int i = 0; i < 10000; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        self.data = [[NSData alloc] init];
    });
}

nonatomic 的不可变类 一般不会崩溃除了 NSData
可变类 都会崩溃

你可能感兴趣的:(iOS)