NSLock + atomic property vs nonatomic

9
我对Objective-C比较新手。如果我有一个类属性,很可能会在诸如API调用之类的异步事件中进行修改,那么确保在另一个线程访问该属性时更改该属性不会导致崩溃的最佳方法是什么?
据我所知,我有两个选择:
1. NSLock + atomic属性
...但这样做似乎每次读写都要锁定该属性,这样就会使其原子性失效。
2. Nonatomic属性
我也可以将其设置为nonatomic,但然后我认为我必须在主线程上进行所有读写操作。是否有一种方法可以作为API调用的结果来完成?成功API响应后回调委托人是否在为该API调用打开的线程上,还是回到主线程上?如果在不同的线程上,我能否将其放回到主线程上?具体而言,我担心NSArray正在遍历它时被更改。
这样做的最佳方法是什么?

把它变成原子级别的就可以了。 - Inder Kumar Rathore
我认为你搞反了。Atomic是线程安全的,而nonatomic则不是。 - Kevin
阅读这篇文章,它将帮助您澄清概念:https://dev59.com/pHRB5IYBdhLWcg3wj32c#589392 - Inder Kumar Rathore
3个回答

4

我想举一个简单的例子,使用Justin的“dispatch APIs”选项:

通过在专用串行队列上执行所有访问操作,可以将共享资源的并发访问变得安全,我们称之为“sync_queue”。

这个“sync_queue”通常是包含要修改的资源的类的私有队列。

现在,您可以定义一个读/写非原子属性,例如:

@property (nonatomic) NSArray* array;

可以如下所示实现访问:

- (void) setArray:(NSArray* newValue) 
{
    dispatch_async(sync_queue, ^{
        _array = newValue;
    });
}

请注意,写访问是异步的。
对属性的“读取”访问将如下实现:
- (NSArray*) array:(NSArray* value) 
{
    if (dispatch_get_specific(SyncQueueID) == sync_queue_id)) {
        return _array;
    }
    else {
        __block NSArray* result = nil;
        dispatch_sync(_sync_queue, ^{
            result = _array;
        });
        return result;
    }     
}

与写访问不同,读访问需要同步。该方法还必须检查当前执行上下文是否不是sync_queue或其子级或任何子孙级队列 - 否则,读取访问将导致死锁。
为了标识当前执行上下文,我们使用函数dispatch_queue_set_specific()在创建时将特定标识符与sync_queue关联起来。稍后,我们使用dispatch_get_specific从当前队列或父代或任何祖先队列获取此标识符。如果返回此特定标识符,则方法分别在sync_queue或其子队列或任何子孙级上执行。如果是这样,方法立即返回值。否则,它会同步地调度到sync_queue上。
注意:
如果UIKit将访问共享资源,则sync_queue应为主队列。

非常棒。我接受这个答案是因为你真的给了我一个解决方案,但还是谢谢大家,所有这些答案都帮了我很多。 - jraede
dispatch_get_specific 调用和分支确实这么安全吗? - tcurdt
@tcurdt 是的,实际上:它使您的方法array独立于调用方所在的执行上下文。假设调用站点已经在sync_queue中执行或者在sync_queue的“子队列”(参见dispatch_set_target_queue)上执行,则调用dispatch_sync(sync_queue,...)将会死锁,否则不会。 - CouchDeveloper
有趣。一开始没有考虑到可能会出现死锁的情况,但是认为这是出于性能原因而疑惑。 - tcurdt

2
如果我有一个类属性,可能会在异步事件(如API调用)期间进行修改,那么确保在另一个线程访问该属性时更改属性不会导致崩溃的最佳方法是什么?
对于可变对象,您需要某种形式的互斥。根据抽象级别和用法,有许多选项。例如:
- `pthread_mutex*` API - `NSLock` API - `@synchronized` - 信号量 - 调度API
NSLock + 原子属性...但是在这种情况下,似乎我必须为每个读写操作锁定属性,这在我看来将使其作为原子设置失去意义。
确切地说,如果您需要锁定每个访问,则“原子”无用。实际上很少使用原子(一种角落情况,其中属性非常简单且独立于任何其他状态)。
更详细地说,您已经提到了NSArray。如果它是一个复制属性(应该是),则原子在实践中可以在极少数情况下允许您通过不可变副本安全地获取/设置数组。但是,在实践中,仅仅拥有指向不可变数组实例的指针的类并不是非常有用;通常,您想要对该数组执行某些操作,并且通常希望以线程安全的方式与对象交互。因此,问题的含义是所讨论的锁也可以用于互斥数组的元素(如果正确执行)。
那么在哪里需要锁定以保证互斥NSMutableArray ivar?设置时,获取时,几乎每次都要向其发送消息。甚至询问其计数或其元素应涉及锁定以消除任何竞争条件。当然,您可以将其包装在更高级别的操作中以确保正确性,并执行这些操作-仅一次获取锁。
非原子属性
原子不会拯救您,非原子也不会。在这种情况下,原子仅使您免受某些潜在的竞争条件的影响。因此,通常应使用非原子,因为您已经需要引入完全的互斥来保证没有竞争条件。
回调到成功的API响应后是否在打开该API调用的线程上,还是回到主线程上?
这取决于API。
如果它在不同的线程上,我能把它放回到主线程吗?
是的,您可以将其添加到主线程的运行循环中或使用调度队列。这是“hacky”的,除非工作需要在特定线程上进行-最明显的情况是在更新AppKit或UIKit视图时。

2

这更像是三个选项:

  1. NSLock
  2. 原子属性
  3. 非原子属性

1)是的,您需要为每次读写锁定该属性。这确实使您能够在整个迭代周围而不仅仅是每个对单元格集合的访问时进行锁定。

2)访问变量时一切都正常(但是,在迭代周围没有锁时,您可能会生成数组被改变的情况,因为正在迭代)。

3)是的,您可以通过回调到主线程来执行所有的读写操作。委托方法是在处理线程还是主线程上调用取决于您所做的调用/使用的框架。您可以使用GCD或perform selector切换回主线程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接