-allKeys on background thread results in error: __NSDictionaryM was mutated while being enumerated

-allKeys on background thread results in error: __NSDictionaryM was mutated while being enumerated

我在后台线程上使用可变字典时遇到了一个有趣的问题。

目前,我正在一个线程上分块下载数据,将其添加到数据集,并在另一个后台线程上处理它。除了一个问题外,整体设计大部分都有效:有时,对主数据集中内部字典的函数调用会导致以下崩溃:

*** Collection <__NSDictionaryM: 0x13000a190> was mutated while being enumerated.

我知道这是一个相当常见的崩溃,但奇怪的是它并没有在此集合的循环中崩溃。相反,Xcode 中的异常断点停止在以下行:

NSArray *tempKeys = [temp allKeys];

这让我相信一个线程正在向这个集合添加项目 ,而 NSMutableDictionary-allKeys 的内部函数调用正在枚举键以便return 另一个线程上的数组.

我的问题是:这是怎么回事?如果是这样,避免这种情况的最佳方法是什么?

这是我正在做的事情的要点:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {

    for (NSString *key in [[queue allKeys] reverseObjectEnumerator]) { //To prevent crashes

        NEXActivityMap *temp = queue[key];
        NSArray *tempKeys = [temp allKeys]; //<= CRASHES HERE
        if (tempKeys.count > 0) {
            //Do other stuff
        }
    }
});

如何使用 @synchronize 将获取密钥的位置和设置密钥的位置换行?

Example:

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}

来自 Apple 文档“Thread safety summary”:

Mutable objects are generally not thread-safe. To use mutable objects in a threaded application, the application must synchronize access to them using locks. (For more information, see Atomic Operations). In general, the collection classes (for example, NSMutableArray, NSMutableDictionary) are not thread-safe when mutations are concerned. That is, if one or more threads are changing the same array, problems can occur. You must lock around spots where reads and writes occur to assure thread safety.

在您的情况下,会发生以下情况。从一个线程,您将元素添加到字典中。在另一个线程中,您访问 allKeys 方法。虽然此方法将所有键复制到数组中,但其他方法会添加新键。这会导致异常。

为避免这种情况,您有多种选择。

因为您正在使用调度队列,所以首选方法是将所有访问相同可变字典实例的代码放入私有串行调度队列。

第二个选项是将不可变字典副本传递给其他线程。在这种情况下,无论原始字典在第一个线程中发生什么,数据仍然是一致的。请注意,您可能需要深层复制,因为您使用 dictionary/arrays 层次结构。

或者,您可以用锁包裹所有访问集合的点。使用 @synchronized 也隐式地为您创建递归锁。

您可以使用 @synchronize。它会起作用的。但这是混淆了两种不同的想法:

  • 线程已经存在很多年了。一个新线程打开一个新的控制流。不同线程中的代码 运行 可能会像您一样同时导致冲突。为了防止这种冲突,你必须使用像 @synchronized 这样的锁。

  • GCD 是更现代的概念。 GCD 运行 "on top of threads" 这意味着它使用线程,但这对您来说是透明的。你不必关心这个。不同队列中的代码 运行 运行 可能同时导致冲突。为了防止这种冲突,您必须为共享资源使用一个队列。

您已经在使用 GCD,what is a good idea:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {

使用线程的相同代码如下所示:

[[NSThread mainThread] performSelector:…];

所以,使用GCD,你应该使用GCD来防止冲突。你正在做的是错误地使用 GCD 然后 "repair" 有锁。

只需将对共享资源的所有访问(在您的例子中是 temp 引用的可变字典)放入串行队列。

  1. 在访问开始时创建一个队列。这是一次性的。

您可以像在代码中那样使用现有队列之一,但您必须使用串行队列之一!但这可能会导致等待任务排长队(在您的示例块中)。串行队列中的不同任务一个接一个地执行,即使有 cpu 个核心空闲。因此,将太多任务放入一个队列并不是一个好主意。为任何共享资源或 "subsystem":

创建一个队列
dispatch_queue_t tempQueue;
tempQueue = dispatch_queue_create("tempQueue", NULL);
  1. 当代码想要访问可变字典时,将其放入队列中:

看起来像这样:

dispatch_sync( tempQueue, // or async, if it is possible
^{
  [tempQueue setObject:… forKey:…]; // Or what you want to do.
}

您必须将每个个访问共享资源的代码放入队列中,因为您必须将每个个访问共享资源的代码放入队列中使用线程时锁定。