Objective c 线程安全:使数据结构不可变是否足够?
我有一个从不同线程访问的类,它修改数组的内容。我开始使用NSMutableArray,但它显然不是线程安全的。用NSArray替换NSMutableArray并在需要时进行复制,这会解决线程安全问题吗 例如: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
@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;
}