线程安全:是否使数据结构不可变?

时间:2014-05-21 13:52:04

标签: objective-c thread-safety immutability

我有一个从不同线程访问的类,它修改了数组的内容。我开始使用NSMutableArray,但它显然不是线程安全的。它会解决线程安全问题,用NSArray替换NSMutableArray并在需要时复制吗?

例如:

@implementation MyClass {
    NSArray *_files;
}

- (void)removeFile:(NSString *)fileName {
    NSMutableArray *mutableFiles = [_files mutableCopy];
    [mutableFiles removeObject:fileName];
    _files = [mutableFiles copy];
}

而不是:

@implementation MyClass {
    NSMutableArray *_files;
}

- (void)removeFile:(NSString *)fileName {
    [_files removeObject:fileName];
}

在我的情况下制作副本并不是那么重要,因为数组会保持很小,并且删除操作不会经常执行。

2 个答案:

答案 0 :(得分:2)

不,它不会,您需要在方法中使用@synchronized来防止多次调用removeFile:并行执行。

像这样:

- (void)removeFile:(NSString *)fileName {
    @synchronized(self)
    {
        [_files removeObject:fileName];
    }
}

它无法使用您的代码的原因是多个线程调用removeFile:同时会导致这种情况发生:

NSMutableArray *mutableFiles1 = [_files mutableCopy]; // Thread 1
[mutableFiles1 removeObject:fileName1];
// Thread 1 is interrupted, Thread 2 is run
NSMutableArray *mutableFiles2 = [_files mutableCopy]; // Thread 2
[mutableFiles2 removeObject:fileName2];
_files = [mutableFiles2 copy];
// Thread 1 is continued
_files = [mutableFiles1 copy];

此时_files仍包含fileName2

这是一种竞争条件,所以它可能看起来没问题,99%的时间都可以工作,但保证是正确的。

答案 1 :(得分:0)

不,这不足以确保线程安全。您必须使用线程编程指南中的各种Synchronization技术概要之一(例如,使用锁,例如NSLock@synchronized)。

或者,通常更高效,您可以使用串行队列来同步对象(请参阅并发编程指南的Migrating Away From Threads章节中的消除基于锁定的代码部分)。虽然@synchronized非常简单,但我倾向于使用专用串行队列的后一种方法来同步访问:

// The private interface

@interface MyClass ()

@property (nonatomic, strong) NSMutableArray   *files;
@property (nonatomic, strong) dispatch_queue_t  fileQueue;

@end

// The implementation

@implementation MyClass

- (instancetype)init
{
    self = [super init];
    if (self) {
        _files = [[NSMutableArray alloc] init];
        _fileQueue = dispatch_queue_create("com.domain.app.files", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)removeFile:(NSString *)fileName
{
    dispatch_async(_fileQueue, ^{
        [_files removeObject:fileName];
    });
}

- (void)addFile:(NSString *)fileName
{
    dispatch_async(_fileQueue, ^{
        [_files addObject:fileName];
    });
}

@end

线程安全的关键是确保与所讨论的对象的所有交互都是同步的。仅使用不可变对象是不够的。只是将removeFile包裹在@synchronized块中也是不够的。您通常希望将所有交互与相关对象同步。您通常不能只返回有问题的对象,让调用者开始使用它而不同步其交互。因此,我可能会提供一种方法,允许调用者以线程安全的方式与此files数组进行交互:

/** Perform some task using the files array
 *
 * @param block This is the block to be performed with the `files` array.
 *
 * @note        This block does not run on the main thread, so if you are doing any
 *              UI interaction, make sure to dispatch that back to the main queue.
 */
- (void)performMutableFileTaskWithBlock:(void (^)(NSMutableArray *files))block
{
    dispatch_sync(_fileQueue, ^{
        block(_files);
    });
}

然后您可以这样称呼:

[myClassObject performMutableFileTaskWithBlock:^(NSMutableArray *files) {
    // do whatever you want with the files array here
}];

就个人而言,它让我有责任让调用者用我的数组做任何想做的事情(我宁愿看MyClass为需要的操作提供一个接口)。但是如果我需要一个线程安全的接口让调用者访问该数组,我可能更喜欢看到这样的块方法,它提供了一个带有数组深层副本的块接口:

/** Perform some task using the files array
 *
 * @param block This is the block to be performed with an immutable deep copy of `files` array.
 */
- (void)performFileTaskWithBlock:(void (^)(NSArray *files))block
{
    dispatch_sync(_fileQueue, ^{
        NSArray *filesDeepCopy = [[NSArray alloc] initWithArray:_files copyItems:YES]; // perform deep copy, albeit only a one-level deep copy
        block(filesDeepCopy);
    });
}

转到你的不可变问题,你可能做的一件事就是有一个方法返回有问题的对象的不可变副本,你可以让调用者按照它认为合适的方式使用它,并理解这表示files数组作为时间快照。 (而且,和上面一样,你会做一个深层复制。)

/** Provide caller with a copy of the files array
 *
 * @return A deep copy of the files array.
 */
- (NSArray *)filesCopy
{
    NSArray __block *filesCopy;
    dispatch_async(_fileQueue, ^{
        filesCopy = [[NSArray alloc] initWithArray:_files copyItems:YES]; // perform deep copy
    });

    return filesCopy;
}
但是很明显,这实际用途有限。例如,在处理文件名数组的情况下,如果这些文件名对应于可能被另一个线程操纵的实际物理文件,则返回该数组的不可变副本可能不合适。但在某些情况下,以上是一个很好的解决方案。它完全取决于所讨论的模型对象的业务规则。