我在背景线程上使用可变字典遇到了一个有趣的问题。
目前,我正在一个线程上以块的形式下载数据,将其添加到数据集,然后在另一个后台线程上处理它。除了一个问题外,整体设计大部分都有效:有时,对主数据集内部字典的函数调用会导致以下崩溃:
*** Collection <__NSDictionaryM: 0x13000a190> was mutated while being enumerated.
我知道这是一个相当常见的崩溃,但奇怪的是它不会在这个集合的循环中崩溃。相反,Xcode中的异常断点在以下行停止:
NSArray *tempKeys = [temp allKeys];
这让我相信一个线程正在向此集合添加项目,而NSMutableDictionary
对-allKeys
的内部函数调用正在枚举键以便返回另一个线程上的数组。
我的问题是:这是怎么回事?如果是这样,那么避免这种情况的最佳方法是什么?
以下是我正在做的事情的要点:
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
}
}
});
答案 0 :(得分:3)
您可以使用@synchronize
。它会奏效。但这混淆了两个不同的想法:
线程已存在多年。新线程打开一个新的控制流。不同线程中的代码可能同时运行,从而导致冲突。要防止此冲突,您必须使用@synchronized
do。
GCD是更现代的概念。 GCD在线程之上运行&#34;这意味着,它使用线程,但这对您来说是透明的。你不必关心这个。在不同队列中运行的代码可能同时运行导致冲突。为了防止这种冲突,您必须使用一个队列来共享资源。
您已使用GCD what is a good idea:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
带线程的相同代码如下所示:
[[NSThread mainThread] performSelector:…];
因此,使用GCD,您应该使用GCD来防止冲突。你正在做的是错误地使用GCD然后&#34;修复&#34;带锁的。
只需将对共享资源的所有访问(在您的情况下为temp
引用的可变字典)放入串行队列中。
您可以像在代码中一样使用其中一个现有队列,但必须使用 serial !但是这可能会导致等待任务的长队(在您的示例块中)。串行队列中的不同任务一个接一个地执行,即使有cpu核空闲也是如此。因此将太多任务放入一个队列并不是一个好主意。为任何共享资源或&#34;子系统&#34;:
创建队列dispatch_queue_t tempQueue;
tempQueue = dispatch_queue_create("tempQueue", NULL);
看起来像这样:
dispatch_sync( tempQueue, // or async, if it is possible
^{
[tempQueue setObject:… forKey:…]; // Or what you want to do.
}
您必须将每个代码访问队列中的共享资源,因为在使用线程时必须将每个代码放入访问共享资源的锁定。
答案 1 :(得分:1)
来自Apple文档&#34; Thread safety summary&#34;:
可变对象通常不是线程安全的。使用可变对象 在线程应用程序中,应用程序必须同步访问 他们用锁。 (有关更多信息,请参阅原子操作)。在 一般来说,集合类(例如,NSMutableArray, NSMutableDictionary)在涉及突变时不是线程安全的。 也就是说,如果一个或多个线程正在更改同一个阵列,则会出现问题 可以发生。您必须锁定发生读写的位置 确保线程安全。
在您的情况下,会发生以下情况。从一个线程,您将元素添加到字典中。在另一个线程中,您访问allKeys
方法。虽然此方法将所有键复制到数组中,但其他方法会添加新键。这会导致异常。
为避免这种情况,您有几种选择。
因为您正在使用调度队列,所以首选方法是将访问相同可变字典实例的所有代码放入专用串行调度队列。
第二个选项是将不可变字典副本传递给其他线程。在这种情况下,无论第一个线程与原始字典发生什么,数据仍然是一致的。请注意,您可能需要深层复制,因为您使用字典/数组层次结构。
或者,您可以使用锁来包装访问集合的所有点。使用@synchronized
也会隐式为您创建递归锁。
答案 2 :(得分:0)
如何使用@synchronize
来包装获取密钥的位置以及设置密钥的位置?
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}