在后台线程中发布的对象太快了

时间:2016-05-05 18:47:39

标签: ios objective-c multithreading memory automatic-ref-counting

我试图在后台线程中构建一个字典数组,同时保持对当前数组的访问,直到后台操作完成。这是我的代码的简化版本:

@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基本上已经过时,所以我没有进一步说明。

4 个答案:

答案 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对象的线程安全问题。

来自Apple文档https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html

  

可变对象通常不是线程安全的。要在线程应用程序中使用可变对象,应用程序必须使用锁同步对它们的访问。 (有关更多信息,请参阅原子操作)。通常,当涉及到突变时,集合类(例如,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]; 
    });
}