在iOS中使类"线程安全"

chi*_*fet 11 objective-c thread-safety ios

我正在阅读关于线程安全的Apple文档,而且我并不完全清楚(在实践中)真正构成一个线程安全的类.为了更好地理解这一点,为了使线程安全(以及为什么),将需要对下面的类进行什么操作?

#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
Run Code Online (Sandbox Code Playgroud)

只需创建一个ivar NSLock,然后锁定/解锁与底层NSMutableArray的所有交互?

长度方法,只是要求数组的计数,也需要这样做吗?

Rob*_*ier 23

使类线程安全的最简单和最好的方法是使其不可变.那你就不必处理任何这个了.它只是有效.值得花时间思考是否需要在多个线程上进行可变性.

但是如果不可变类为您的设计带来了重大问题,那么通常最好的实现方法是使用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
Run Code Online (Sandbox Code Playgroud)

请注意dispatch_sync所有读者和dispatch_barrier_async所有作者的使用.这是通过允许并行读取器和独占编写器将开销降至最低的方法.如果没有争(这是正常的情况下),dispatch_sync比锁(低得多的开销NSLock或者@synchronized甚至并行线程锁).

有关如何在Cocoa中更好地处理并发性的更多建议,请参阅从线程迁移.

  • @chinabuffet如果另一个线程`入队`,但忽略更新其`length`属性怎么办?如果那个其他线程正在更新`length`属性的过程中,更新了`length`的内部表示的第一个字节,但是还没有更新第二个字节呢?通过将读者与作者同步,您可以保护自己免受此攻击.读写器模式的优点在于,使用并发队列,写入与屏障同步,但允许读取与其他读取同时发生:在需要时同步,但不是. (2认同)
  • 我担心您会误解数组多线程的工作原理。一个线程正在读取数组,而另一个线程附加到该数组是不安全的。它与访问同一元素无关。同时读取和写入数组的任何部分都是未定义的行为。追加到数组可能会导致整个数组重新分配并移至内存的另一部分。如果您尝试同时读取它,则可能会发现无效的内存。这不是一个奇怪的情况;它一直在发生。 (2认同)

Pau*_*w11 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
Run Code Online (Sandbox Code Playgroud)


Rob*_*Rob 5

最重要的是,线程安全的第一步是确保在尝试从另一个线程访问对象时,没有一个线程使对象变异(使其处于可能不一致的状态).因此,任何各种同步技术都是有用的.有关各种类型机制的详细信息,请参阅" 线程编程指南"的" 同步"部分.Rob Napier演示的读写器模式远比指令更有效,并且在WWDC 2012视频异步设计模式与块,GCD和XPC中进行了讨论.@synchronizedNSLock

顺便说一句,在peek和length方法在多线程环境下已经减少的效用.这些暗示了天真的开发人员可能错误地推断这些方法和其他方法之间的依赖关系.例如,仅仅因为length大于零并不意味着当你随后去检索它时,任何东西都会存在.

我会仔细研究这些方法,并问自己在多线程环境中是否有意义.我知道你可能只是意味着这些是可变数组中线程安全的任意例子,但它表明了我在"线程安全"示例中经常看到的更广泛的问题,我在其他地方看到Stack Overflow,其中同步机制通常是在错误的水平上承担任何效用.

  • 我的观点是,如果您按照纳皮尔或保罗的答案中所设想的那样同步"长度",那么您所保证的只是返回的数字是有效的,并且内部与队列对象的状态一致_在调用`length`时.但返回的值也会立即过时,因为在调用线程使用返回值时,另一个线程可能已经改变了队列对象.在这种情况下,解决方案可能是弃用`length`和`peek`,并且只有`enqueue`和`dequeue`方法. (2认同)
  • 类比为`viewWillAppear`方法:通过引入可取消的交互式动画过渡,您不再保证`viewDidAppear`将跟随`viewWillAppear`。在WWDC上,他们开玩笑说“ viewWillAppear”可以重命名为“ viewMightAppear”或“ viewWillProbablyAppear”或“ iReallyWishThisViewWouldAppear”。因此,按照这种类推,“长度”方法可能称为“ lengthUsedToBeButCouldBeDifferentNow”。大声笑。 (2认同)