在 iOS 中制作 class "thread safe"

making a class "thread safe" in iOS

我正在阅读有关线程安全的 Apple docs,但我并不完全清楚什么(在实践中)真正构成了 class 线程安全。为了帮助更好地理解这一点,需要对以下 class 做些什么才能使其线程安全(以及为什么)?

#import "UnsafeQueue.h"

@interface UnsafeQueue()

@property (strong, nonatomic) NSMutableArray *data;

@end

@implementation UnsafeQueue

- (id)peek {
    return [self.data firstObject];
}

- (NSUInteger)length {
    return [self.data count];
}

- (void)enqueue:(id)datum {
    [self.data addObject:datum];
}

// other methods omitted...

@end

是否会简单地创建一个 ivar NSLock,然后 lock/unlock 围绕 所有 与底层 NSMutableArray 的交互?

只要求数组计数的长度方法是否也需要这样做?

线程安全意味着数据结构可以被多个线程访问and/or修改而不会损坏。

一种简单的方法是使用 Objective-C 的 @synchronized 功能。

在这种情况下,@synchronized(self.data) 围绕您对数组的所有访问将确保一次只有一个线程可以访问该数组。

即使 length 不修改数组,您仍然需要保护它的访问,因为另一个线程可能会修改数组 -

#import "SafeQueue.h"

@interface SafeQueue()

@property (strong, nonatomic) NSMutableArray *data;

@end

@implementation SafeQueue

- (id)peek {
    @synchronized (self.data) {
        return [self.data firstObject];
    }
}

- (NSUInteger)length {
    @synchronized(self.data) {
        return [self.data count];
    }
}

- (void)enqueue:(id)datum {
    @synchronized(self.data) {
        [self.data addObject:datum];
    }
}

// other methods omitted...

@end

使 class 线程安全的最简单和最好的方法是使其不可变。那么你不必处理任何这些。它只是工作。确实值得花时间考虑是否需要多线程可变性。

但是如果不可变 class 会给您的设计带来重大问题,那么通常最好的实现方式是使用 GCD 而不是锁。 GCD 的开销要低得多,一般来说更容易正确。

在这种特殊情况下,我会按照这些路线实施它(未经测试,我已经在 Swift 中工作了一段时间,所以如果我删除分号请原谅我):

#import "SafeQueue.h"

@interface SafeQueue()

@property (strong, nonatomic) NSMutableArray *data;
@property (strong, nonatomic) dispatch_queue_t dataQueue;

@end

@implementation SafeQueue

- (instancetype)init {
    if (self = [super init]) {
        _dataQueue = dispatch_queue_create("SafeQueue.data", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (id)peek {
    __block id result = nil;
    dispatch_sync(self.dataQueue, ^{ result = [self.data firstObject] });
    return result;
}

- (NSUInteger)length {
    __block NSUInteger result = 0;
    dispatch_sync(self.dataQueue, ^{ result = [self.data count] });
    return result;
}

- (void)enqueue:(id)datum {
    dispatch_barrier_async(self.dataQueue, ^{ [self.data addObject:datum] });
}

// other methods omitted...

@end

请注意对所有读者使用 dispatch_sync,对所有作者使用 dispatch_barrier_async。这就是通过允许并行读取器和独占写入器来将开销降至最低的方法。如果没有争用(这是正常情况),dispatch_sync 比锁(NSLock@synchronized 甚至 pthreads 锁)的开销要低得多。

有关如何在 Cocoa 中更好地处理并发的更多建议,请参阅 Migrating Away from Threads

最重要的是,线程安全的第一步是确保您没有一个线程在尝试从另一个线程访问对象时改变对象(它处于可能不一致的状态)。因此,各种同步技术中的任何一种都是有用的。参见 Synchronization section of the Threading Programming Guide for more information on a variety of types of mechanisms. The reader-writer pattern illustrated by Rob Napier is far more efficient than the @synchronized directive or NSLock and is discussed in WWDC 2012 video Asynchronous Design Patterns with Blocks, GCD, and XPC

顺便说一下,peeklength 方法在多线程环境中的实用性有所降低。这些表明天真的开发人员可能会在这些方法和其他方法之间错误地推断出一种依赖关系。例如,仅仅因为 length 大于零并不意味着当您随后去检索它时,任何东西都会在那里。

我会仔细研究这些方法并问问自己它们在多线程环境中是否有意义。我知道你可能只是说这些是可变数组中线程安全的任意示例,但这表明我经常在 "thread-safe" 示例中看到更广泛的问题,我在 Stack Overflow 的其他地方看到过,其中同步机制通常是在错误的水平承担任何效用。