我正在阅读围绕线程安全的Apple docs,并且我并不完全清楚(在实践中)真正构成一个线程安全的类。为了更好地理解这一点,为了使线程安全(以及为什么),将需要对下面的类进行什么操作?
#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,然后锁定/解锁周围的与底层NSMutableArray的所有交互?
只是要求数组计数的长度方法是否也需要这样做?
答案 0 :(得分:22)
使类线程安全的最简单和最好的方法是使其不可变。那你就不必处理任何这个了。它只是有效。值得花些时间考虑一下你是否需要在多个线程上进行可变性。
但是如果一个不可变类为你的设计带来了重大问题,那么通常最好的实现方法是使用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。
答案 1 :(得分:5)
线程安全意味着可以通过多个线程访问和/或修改数据结构而不会损坏。
一种简单的方法是使用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
答案 2 :(得分:5)
最重要的是,线程安全的第一步是确保在您尝试从另一个线程访问对象时,没有一个线程使对象发生变异(使其处于可能不一致的状态)。因此,任何各种同步技术都是有用的。有关各种类型机制的更多信息,请参阅线程编程指南的Synchronization部分。 Rob Napier演示的读写器模式比@synchronized
指令或NSLock
更有效,并在WWDC 2012视频Asynchronous Design Patterns with Blocks, GCD, and XPC中进行了讨论。
顺便说一句,peek
和length
方法在多线程环境中的实用性降低了。这些暗示了天真的开发人员可能错误地推断这些方法和其他方法之间的依赖关系。例如,仅仅因为length
大于零并不意味着当你随后去检索它时,任何东西都会存在。
我会仔细研究这些方法,并问自己在多线程环境中是否有意义。我知道你可能只是意味着这些是可变数组中线程安全的任意例子,但它表明了我在“线程安全”示例中经常看到的更广泛的问题,我在其他地方看到Stack Overflow,其中同步机制通常是在错误的水平上承担任何效用。