我试图在后台线程中构建一个字典数组,同时保持对当前数组的访问,直到后台操作完成。这是我的代码的简化版本:
@property (nonatomic, strong) NSMutableArray *data;
@property (nonatomic, strong) NSMutableArray *dataInProgress;
- (void)loadData {
self.dataInProgress = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[self loadDataWorker];
});
}
- (void)loadDataWorker {
for (int i=0; i<10000; i++) {
[self addDataItem];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self loadDataFinish]; // the crash occurs before we get to this point
});
}
- (void)addDataItem {
// first check some previously added data
int currentCount = (int)[self.dataInProgress count];
if (currentCount > 0) {
NSDictionary *lastItem = [self.dataInProgress objectAtIndex:(currentCount - 1)];
NSDictionary *checkValue = [lastItem objectForKey:@"key3"]; // this line crashes with EXC_BAD_ACCESS
}
// then add another item
NSDictionary *dictionaryValue = [NSDictionary dictionaryWithObjectsAndKeys:@"bar", @"foo", nil];
NSDictionary *item = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", @"value2", @"key2", dictionaryValue, @"key3", nil];
// as described in UPDATE, I think this is the problem
dispatch_async(dispatch_get_main_queue(), ^{
[dictionaryValue setObject:[self makeCustomView] forKey:@"customView"];
});
[self.dataInProgress addObject:item];
}
- (UIView *)makeCustomView {
return [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
}
- (void)loadDataFinish {
self.data = [NSMutableArray arrayWithArray:self.dataInProgress];
}
这在大多数情况下都可以正常工作,但是当数据集很大时,我开始在上面指定的行上崩溃。对于更多数据或具有更少内存的设备,崩溃的可能性更大。在包含10,000件物品的iPhone 6上,它大约有五分之一。所以看起来当内存变得紧张时,数据数组中的字典会在我访问之前被销毁。
如果我在主线程中执行所有操作,则不会发生崩溃。我最初使用非ARC代码遇到了这个问题,然后我将我的项目转换为ARC,同样的问题仍然存在。
有没有办法确保在构建过程的早期添加的对象保留到我完成之后?或者有更好的方法来做我正在做的事情吗?
这是一个堆栈跟踪:
thread #17: tid = 0x9c586, 0x00000001802d1b90 libobjc.A.dylib`objc_msgSend + 16, queue = 'com.apple.root.background-qos', stop reason = EXC_BAD_ACCESS (code=1, address=0x10)
frame #0: 0x00000001802d1b90 libobjc.A.dylib`objc_msgSend + 16
frame #1: 0x0000000180b42384 CoreFoundation`-[__NSDictionaryM objectForKey:] + 148
frame #2: 0x00000001002edd58 MyApp`-[Table addDataItem](self=0x000000014fd44600, _cmd="addDataItem", id=0x00000001527650d0, section=3, cellData=0x0000000152765050) + 1232 at Table.m:392
frame #4: 0x00000001002eca28 MyApp`__25-[Table loadData]_block_invoke(.block_descriptor=0x000000015229efd0) + 52 at Table.m:265
frame #5: 0x0000000100705a7c libdispatch.dylib`_dispatch_call_block_and_release + 24
frame #6: 0x0000000100705a3c libdispatch.dylib`_dispatch_client_callout + 16
frame #7: 0x0000000100714c9c libdispatch.dylib`_dispatch_root_queue_drain + 2344
frame #8: 0x0000000100714364 libdispatch.dylib`_dispatch_worker_thread3 + 132
frame #9: 0x00000001808bd470 libsystem_pthread.dylib`_pthread_wqthread + 1092
frame #10: 0x00000001808bd020 libsystem_pthread.dylib`start_wqthread + 4
更新
我通过我的完整代码跟踪了下面的答案,特别是关于多线程锁定的那些,并意识到我添加到我的数据阵列的部分数据是我创建的UIView在构建过程中。由于在后台线程中构建视图很糟糕,而且在执行此操作时我确实发现了问题,因此我跳回到makeCustomView的主线程。请参阅上面添加的代码行&#34; UPDATE&#34;在评论中。这一定是现在的问题;当我跳过添加自定义视图时,我没有更多的崩溃。
我可以重新构建构建工作流,以便在后台线程上添加除自定义视图之外的所有数据,然后我可以进行第二次传递并在主线程上添加自定义视图。但有没有办法管理此工作流程中的线程?我在调用makeCustomView之前和之后尝试使用NSLock锁定,但这没有任何区别。我还发现SO answer说NSLock基本上已经过时,所以我没有进一步说明。
答案 0 :(得分:2)
如果我理解正确,对dataInProgress
数组的并发访问会导致问题,因为数组填充在后台线程中并在主线程中使用。但NSMutableArray
不是线程安全的。这符合我的意图,即阵列本身已损坏。
你可以用NSLock
解决这个问题,以序列化对数组的访问,但这类似于过时的,并不适合你的代码的其余部分,后者使用更现代(更好)的GCD。 / p>
一个。情况
你有什么:
dataInProgress
)B中。 GCD
使用经典的线程/锁定方法,当它们同时访问共享资源时,启动异步控制流并使用锁序序化它们。
使用GCD,您可以同时启动控制流,但是为给定的共享资源进行序列化。 (基本上,有更多功能,更复杂,但这就是我们在这里需要的。)
℃。序列化
在后台队列(&#34; thread&#34;)中启动构建器以在不阻塞主线程的情况下运行它是正确的。完成。
如果要对UI元素执行某些操作,尤其是切换回主线程是正确的。创建一个视图。
由于两个控制流都访问相同的资源,因此必须序列化访问。您可以通过为该资源创建(串行)队列来执行此操作:
…
@property dispatch_queue_t dataInProgressAccessQ;
…
// In init or whatever
self. dataInProgressAccessQ = dispatch_queue_create("com.yourcompany.dataInProgressQ", NULL);
执行此操作后,您将每次访问该队列中的dataInProgress
数组。有一个简单的例子:
// [self.dataInProgress addObject:item];
dispatch_async( self.dataInProgressAccessQ,
^{
[self.dataInProgress addObject:item];
});
在这种情况下,它非常简单,因为您必须在代码和代码处切换队列。如果它在中间,你有两个选择:
a)使用类似于锁的队列。让我们举个例子:
// NSInteger currentCount = [self.dataInProgress count]; // Why int?
NSInteger currentCount;
dispatch_sync( self.dataInProgressAccessQ,
^{
currentCount = [self.dataInProgress count];
});
// More code using currentCount
使用dispatch_sync()
将让代码执行等待,直到完成其他控制流的访问。 (这就像一把锁。)
编辑:与锁一样,保证访问序列化。但是可能存在问题,即另一个线程从数组中删除对象。让我们来看看这种情况:
// NSInteger currentCount = [self.dataInProgress count]; // Why int?
NSInteger currentCount;
dispatch_sync( self.dataInProgressAccessQ,
^{
currentCount = [self.dataInProgress count];
});
// More code using currentCount
// Imagine that the execution is stopped here
// Imagine that -makeCustomView removes the last item in meanwhile
// Imagine that the execution continues here
// -> currentCount is not valid anymore.
id lastItem = [self.dataInProgress objectAtIndex:currentCount]; // crash: index out of bounds
为了防止这种情况,您必须隔离并发代码。这很大程度上取决于您的代码。但是,在我的例子中:
id lastItem;
dispatch_sync( self.dataInProgressAccessQ,
^{
NSInteger currentCount;
currentCount = [self.dataInProgress count];
lastItem = [self.dataInProgress objectAtIndex:currentCount]; // don't crash: bounds are not changed
});
// Continue with lastItem
您可以想象,在获取最后一项时,如果可以在您阅读之后的下一刻从数组中删除。也许这会导致代码不一致的问题。这真的取决于你的代码。
编辑结束
b)也许你会遇到性能问题,因为它就像锁(同步)一样。如果是这样,您必须分析代码并提取可以再次并发运行的部分。模式看起来像这样:
// NSInteger currentCount = [self.dataInProgress count]; // Why int?
dispatch_async( self.dataInProgressAccessQ, // <-- becomes asynch
^{
NSInteger currentCount = [self.dataInProgress count];
// go back to the background queue to leave the access queue fastly
dispatch_async( dispatch_get_global_queue(),
^{
// use current count here.
});
});
dispatch_async( self.dataInProgressAccessQ,
^{
// Another task, that can run concurrently to the above
});
您可以在那里做什么,这取决于您的具体代码。也许这对您有帮助,拥有自己的私有构建器队列而不是使用全局队列。
但这是基本方法:将任务移动到队列中,不等待,直到完成,但最后添加代码,完成另一个控制流中的任务。
而不是
Code
--lock--
var = Access code
--unlock--
More Code using var
它是
Code
asynch {
var Access Code
asynch {
More code using var
}
}
当然,你必须在-makeCustomView
内做同样的事情。
答案 1 :(得分:1)
我同意菲利普·米尔斯的观点。这看起来像是self.dataInProgress
对象的线程安全问题。
可变对象通常不是线程安全的。要在线程应用程序中使用可变对象,应用程序必须使用锁同步对它们的访问。 (有关更多信息,请参阅原子操作)。通常,当涉及到突变时,集合类(例如,NSMutableArray,NSMutableDictionary)不是线程安全的。也就是说,如果一个或多个线程正在更改同一阵列,则可能会出现问题。您必须锁定发生读写的位置,以确保线程安全。
如果从各种后台线程调用addDataItem
,则需要锁定读写self.dataInProgress
。
答案 2 :(得分:0)
我认为你不需要深刻的副本。如果字典不可变,那么你需要的只是让它们不被释放......而且它们所在的数组的副本将为你做这些。
我认为您需要的是围绕self.data
访问权限的同步。我建议为您的类创建一个NSLock
对象,并使用锁定/解锁方法调用包装以下两行中的每一行:
self.data = [NSMutableArray arrayWithArray:self.dataInProgress];
//...
NSDictionary *item = [self.data objectAtIndex:index];
另外,为什么self.data
需要是可变的?如果没有,self.data = [self.dataInProgress copy];
更简单......并且对内存和性能的效率可能更高。
令我担心的一件事是,getData
的来电怎么样?它可能不知道self.data
数组已更改。如果数组变得更短,你就会走向“索引越界”崩溃。
当您知道阵列将变得稳定时,最好只调用getData
。 (换句话说,同步数据会获得更高的级别。)
答案 3 :(得分:0)
我会尝试传递对自我的弱引用。我敢打赌,如果你可能在那里发生强烈的保留周期。如果我没记错的话,__weak
没有保留计数,__block
允许您更改变量
- (void)loadData {
self.dataInProgress = [NSMutableArray array];
__weak __block SelfClassName *weakSelf = self;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[weakSelf loadDataWorker];
});
}
- (void)loadDataWorker {
for (int i=0; i<10000; i++) {
[self addDataItem];
}
__weak __block SelfClassName *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf loadDataFinish];
});
}