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

Objective c 线程安全:使数据结构不可变是否足够?,objective-c,thread-safety,immutability,Objective C,Thread Safety,Immutability,我有一个从不同线程访问的类,它修改数组的内容。我开始使用NSMutableArray,但它显然不是线程安全的。用NSArray替换NSMutableArray并在需要时进行复制,这会解决线程安全问题吗 例如: @implementation MyClass { NSArray *_files; } - (void)removeFile:(NSString *)fileName { NSMutableArray *mutableFiles = [_files mutableCop

我有一个从不同线程访问的类,它修改数组的内容。我开始使用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];
}

在我的例子中,制作副本并不那么重要,因为数组将保持非常小,并且删除操作不会经常执行。

不,不会的,您需要在方法中使用@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];
此时_文件仍然包含
fileName2


这是一个争用条件,因此它看起来可以正常工作99%的时间,但不能保证它是正确的。

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

或者,通常更有效的是,您可以使用串行队列来同步对象(请参阅《并发编程指南》一章中的消除基于锁的代码部分)。虽然
@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
线程安全的关键是确保与相关对象的所有交互都是同步的。仅仅使用不可变对象是不够的。仅仅在
@synchronized
块中包装
removeFile
也是不够的。您通常希望与相关对象同步所有交互。通常,您不能只返回有问题的对象,让调用者在不同步其交互的情况下开始使用它。因此,我可以提供一种方法,允许调用者以线程安全的方式与这个
文件
数组交互:

/** 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);
    });
}
至于不可变的问题,您可以做的一件事是使用一个方法返回所讨论对象的不可变副本,您可以让调用者使用它认为合适的副本,并理解这将
文件
数组及时表示为快照。(如上文所述,您需要进行深度复制。)


但显然,这限制了实际应用。例如,当您处理一个文件名数组时,如果这些文件名对应于可能由另一个线程操作的实际物理文件,则可能不适合返回该数组的不可变副本。但在某些情况下,上述方法是一个很好的解决方案。这完全取决于所讨论的模型对象的业务规则。

如果您真的想归档一个无锁线程安全,那么这篇文章会更合适:我刚才听说,“不可变的数据存储和单写多读”是可能的。但您的访问模型看起来不像“单编写器”访问模型。如果我是你,我就用@synchronize(this);-)这将是一种罕见的情况(在Apple Objective-C可以瞄准的任何平台上可能都不可能),但一般来说,您不能假设指针分配是一种原子操作。实际上可能需要多个处理器指令来编写整个过程,在这种情况下,同步读卡器线程有机会读取一个垃圾值,该值由旧指针值的一部分和新指针值的一部分组成。现在,如果我保留不可变版本,使其成为原子属性,不创建其他锁,如果我不关心数组的内容有时可能有点错误,我能确定它不会崩溃吗?它认为应该这样做,如果你可以把你的方法有时什么都不起作用这一事实称为“工作”的话。我想知道你为什么认为用锁很糟糕?
/** 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;
}