Objective-C的NSMutableArray线程安全吗?

53

我已经试图解决这个崩溃问题将近一周了。应用程序在没有任何异常或堆栈跟踪的情况下崩溃。在僵尸模式下通过仪器运行时,应用程序不会以任何方式崩溃。

我有一个在不同线程上调用的方法。 解决崩溃的方案是替换:

[self.mutableArray removeAllObjects];

使用

dispatch_async(dispatch_get_main_queue(), ^{
    [self.searchResult removeAllObjects];
});

我认为这可能是一个时间问题,所以我尝试进行同步,但它仍然崩溃了:

@synchronized(self)
{
    [self.searchResult removeAllObjects];
}

这里是代码

- (void)populateItems
{
   // Cancel if already exists  
   [self.searchThread cancel];

   self.searchThread = [[NSThread alloc] initWithTarget:self
                                               selector:@selector(populateItemsinBackground)
                                                 object:nil];

    [self.searchThread start];
}


- (void)populateItemsinBackground
{
    @autoreleasepool
    {
        if ([[NSThread currentThread] isCancelled])
            [NSThread exit];

        [self.mutableArray removeAllObjects];

        // Populate data here into mutable array

        for (loop here)
        {
            if ([[NSThread currentThread] isCancelled])
                [NSThread exit];

            // Add items to mutableArray
        }
    }
}

NSMutableArray是否不具备线程安全性?


3
一个单独的 @synchronized 块并不能起到任何作用。你需要在每次访问数组时都使用锁(这正是 @synchronized 提供的功能)。 - jscs
1
线程安全和不安全类列表 https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html - Jageen
为什么会出现@autoreleasepool?有人能给我解释一下吗? - MuraliMohan
如果您在应用程序工具包的主线程之外进行Cocoa调用,例如创建仅基础框架的应用程序或分离线程,则需要创建自己的自动释放池。@MuraliMohan https://developer.apple.com/reference/foundation/nsautoreleasepool - aryaxt
1
在我看来,处理读写访问问题的最佳方式是使用GCD(Grand Central Dispatch)提供的dispatch barriers。 - XLE_22
7个回答

90

不支持多线程,如果你需要在另一个线程修改可变数组,应该使用NSLock来确保一切按计划进行:

NSLock *arrayLock = [[NSLock alloc] init];

[...] 

[arrayLock lock]; // NSMutableArray isn't thread-safe
[myMutableArray addObject:@"something"];
[myMutableArray removeObjectAtIndex:5];
[arrayLock unlock];

33
你需要这样做,并且还需要用同样的锁来保护每个读取。 - Adam Rosenfield
1
如果@property声明时没有使用nonatomic修饰符,那么这是否必要? - Mark Adams
1
你是怎么做到的?你把 removeAllObjects 包围在锁里了吗?你的锁在哪里初始化了? - Daniel
2
是的,你应该从初始化想要锁定的对象的线程中初始化锁。尝试将锁放在初始化数组旁边,并将其设置为实例变量。 - Daniel
首先,感谢你的回复!对于读取操作,你可以使用[mutableArray copy]。 - digipeople
显示剩余4条评论

39

正如其他人所说,NSMutableArray不是线程安全的。如果有人想在线程安全的环境中实现多于removeAllObject的操作,我将提供另一种使用GCD的解决方案,除了使用锁之外。您需要做的是同步读取/更新(替换/删除)操作。

首先获取全局并发队列:

dispatch_queue_t concurrent_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

读取数据:

- (id)objectAtIndex:(NSUInteger)index {
    __block id obj;
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.searchResult objectAtIndex:index];
    });
    return obj;
}

插入操作:

- (void)insertObject:(id)obj atIndex:(NSUInteger)index {
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.searchResult insertObject:obj atIndex:index];
    });
}

来自苹果文档关于dispatch_barrier_async:

当屏障块到达私有并发队列的前面时,它不会立即执行。相反,该队列将等待其当前正在执行的块完成执行。在此时,屏障块单独执行。任何在屏障块之后提交的块都将等待屏障块完成才会被执行。

remove操作类似:

- (void)removeObjectAtIndex:(NSUInteger)index {
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.searchResult removeObjectAtIndex:index];
    });
}

编辑: 今天我发现了另一种更简单的方式来使用GCD提供的串行队列来同步访问资源。

从苹果文档《并发编程指南》>调度队列中得知:

串行队列在想要按特定顺序执行任务时非常有用。串行队列一次只执行一个任务,并始终从队列头部获取任务。您可以使用串行队列来保护共享资源或可变数据结构,而不是使用锁。与锁不同,串行队列确保任务按可预测的顺序执行。只要您将任务异步提交给串行队列,队列就永远不会死锁。

创建您的串行队列:

dispatch_queue_t myQueue = dispatch_queue_create("com.example.MyQueue", NULL);

将任务异步派发到串行队列:

dispatch_async(myQueue, ^{
    obj = [self.searchResult objectAtIndex:index];
});

dispatch_async(myQueue, ^{
    [self.searchResult removeObjectAtIndex:index];
});

希望对你有所帮助!


1
使用gcd可能会导致问题。如果期望在运行循环结束之前从错误中删除/添加值,那该怎么办? - aryaxt
只需在同一串行队列上执行 所有 操作。 - gnasher729
串行队列当然是一个解决方案,但是从任何类型的集合中读取线程安全的。并行读取和障碍修改也完全没有问题。 - Tricertops
14
Jingjie,你的原始答案使用"barrier"是一个很好的答案,因为这比被接受的答案的锁方法更有效率。它也比串行队列更好。将来的读者应该在WWDC 2012视频——《使用块、GCD和XPC的异步设计模式》中查看"Reader-Writer"讨论。注意,然而,永远不应该在全局队列上使用barriers(请注意引用"private"队列)。使用自定义并发队列,例如concurrent_queue = dispatch_queue_create("com.domain.queuename", DISPATCH_QUEUE_CONCURRENT); - Rob
4
强调Rob所说的,在全局队列上不应该使用dispatch_barrier,如果使用了也无法正常工作。苹果官方文档中表示:“你指定的队列应该是使用dispatch_queue_create函数自己创建的并发队列。如果你传递给此函数的队列是串行队列或全局并发队列之一,此函数的行为就像dispatch_async函数一样。” - scosman
我正在使用带有信号量的第一种解决方案。没有使用信号量时,我曾经遇到过死锁问题。信号量计数为20。 - Parag Bafna

21
除了使用 NSLock,你也可以使用 @synchronized(条件对象) 来实现线程安全,需要确保每次访问数组时都使用同一个对象作为 条件对象 进行 @synchronized 包装。如果只想修改同一个数组实例的内容,则可以使用数组本身作为 条件对象;否则,需要使用其它不会被销毁的对象(如 self),因为对于同一个数组来说,self 对象始终是相同的。
@property 属性中使用 atomic 只会使得设置数组是线程安全的,而不是修改数组的内容。例如 self.mutableArray = ... 是线程安全的,但 [self.mutableArray removeObject:] 不是。

14
__weak typeof(self)weakSelf = self;

 @synchronized (weakSelf.mutableArray) {
     [weakSelf.mutableArray removeAllObjects];
 }

仅供记录,你的同步语句本身没有任何意义。如果你将其包装在读写操作中以避免竞态条件,则它是有意义的,但是包装 removeAllObjects 方法并没有增加任何价值。这应该在更高级别的用例中完成。 - GoRoS
@GoRoS 不确定为什么你觉得这没什么意义,因为它创建了互斥锁并防止不同的线程访问这个数组。 - sash
这里是关于我所说的内容的简要解释:https://dev59.com/nnvaa4cB1Zd3GeqPB1WY#21139721 - GoRoS
@GoRos中的synchronized仅防止多个线程同时访问数组,而不能防止逻辑错误。基本上,如果您在一个线程上删除所有对象,然后尝试在另一个线程上访问任何对象,您仍将导致崩溃。同步只会防止删除和访问对象不会在完全相同的时间发生。您可以在同步块内执行多个操作。请阅读文章。 - sash
抱歉,我完全不同意。如果您删除所有带有答案代码的对象并从另一个元素访问它,它仍然会崩溃。应该是其他线程的代码块具有更高业务级别用例来锁定可变数组,执行读/写操作并解锁它。在这些操作之间将不会执行任何操作。在您的链接中,同步块包装了读取和写入操作,这是有意义的。请查看先前的评论链接以了解我为什么说它是错误的。 - GoRoS
是的,它会崩溃,因为这是逻辑错误。在进行任何操作之前,您应该检查数组的长度。此示例仅确保在执行removeAllObjects时,没有其他线程访问此数组。抱歉,我无法向您解释得更好 :) - sash

5

提到串行队列:使用可变数组时,仅仅问“它是否线程安全”是不够的。例如,确保 removeAllObjects 不会崩溃是好的,但如果另一个线程尝试同时处理该数组,则它将在所有元素被删除之前或之后处理该数组,并且您真的必须考虑行为应该是什么。

创建一个负责此数组的类和对象,为其创建一个串行队列,并通过该类在该串行队列上执行所有操作,是最简单的方法,可以在不通过同步问题使大脑疼痛的情况下正确地完成事情。


1

所有NSMutablexxx类都不是线程安全的。包括获取、插入、删除、添加和替换等操作应该使用NSLock。这是苹果提供的线程安全和线程不安全类的列表:Thread Safety Summary


0

几乎所有 NSMutable 类的对象都不是线程安全的。


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