我有一个从不同线程访问的类,它修改了数组的内容。我开始使用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];
}
在我的情况下制作副本并不是那么重要,因为数组会保持很小,并且删除操作不会经常执行。
答案 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;
}
但是很明显,这实际用途有限。例如,在处理文件名数组的情况下,如果这些文件名对应于可能被另一个线程操纵的实际物理文件,则返回该数组的不可变副本可能不合适。但在某些情况下,以上是一个很好的解决方案。它完全取决于所讨论的模型对象的业务规则。