在后台线程中对象释放得太快

Objects released too soon while in a background thread

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

@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

更新

我根据下面的答案追踪了我的完整代码,特别是那些关于多线程时锁定的代码,并意识到我添加到我的数据数组的部分数据是我在构建过程。因为在后台线程中构建视图是不好的,而且我在这样做时确实看到了问题,所以我跳回到 makeCustomView 的主线程。请参阅我在上面添加的代码行,并在注释中添加了 "UPDATE"。这一定是现在的问题;当我跳过添加自定义视图时,我不再崩溃了。

我可以重新构建工作流程,以便将除自定义视图之外的所有数据都添加到后台线程,然后我可以进行第二次传递并在主线程中添加自定义视图。但是有没有办法管理这个工作流中的线程呢?我尝试在调用 makeCustomView 之前和之后使用 NSLock 进行锁定,但这没有任何区别。我还发现 SO answer 说 NSLock 基本上已经过时了,所以我没有进一步讨论。

我认为您不需要深拷贝。如果字典不是可变的,您所需要的就是不释放它们……它们所在的数组的副本将为您做到这一点。

我认为您需要的是围绕对 self.data 的任何访问进行同步。我建议为您的 class 创建一个 NSLock 对象,并用 lock/unlock 方法调用包装以下两行:

self.data = [NSMutableArray arrayWithArray:self.dataInProgress];
//...
NSDictionary *item = [self.data objectAtIndex:index];

此外,为什么 self.data 需要可变?如果没有,self.data = [self.dataInProgress copy]; 会更简单……而且很可能在内存和性能方面更高效。


让我担心的一件事是,getData 的调用者怎么办?它可能不知道 self.data 数组发生了变化。如果数组变短,您将面临 "index out of bounds" 崩溃。

最好只在知道数组将稳定时才调用 getData。 (换句话说,在更高的层次上同步数据。)

我会尝试传递对自身的弱引用。我敢打赌,如果你在某处发生了一个强大的保留周期。如果我没记错的话,__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]; 
    });
}

我同意菲利普米尔斯的观点。这看起来像是围绕您的 self.dataInProgress 对象的线程安全问题。

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

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.

如果从各种后台线程调用 addDataItem,您需要锁定对 self.dataInProgress 的读写。

如果我没理解错的话,并发访问 dataInProgress 数组会导致问题,因为该数组在后台线程中填充并在主线程中使用。但是 NSMutableArray 不是线程安全的。这符合我的意图,即数组本身已损坏。

您可以使用 NSLock 序列化对数组的访问来解决这个问题,但这有点过时并且不适合您的其余代码,它使用更现代(和更好)的 GCD .

一个。情况

你有:

  • 一个构建器控制流程,必须在后台运行
  • 创建视图控制流,它必须在主队列(线程)中运行。 (我不完全确定,视图的纯创建是否必须在主线程中完成,但我会这样做。)
  • 两个控制流访问相同的资源(dataInProgress)

乙。 GCD

使用经典的 thread/lock 方法,当它们同时访问共享资源时,您启动异步控制流并使用锁序列化它们。

使用 GCD 时,您可以同时启动彼此之间的控制流,但会针对给定的共享资源进行序列化。 (基本上,功能更多,更复杂,但这就是我们在这里需要的。)

摄氏度。序列化

在后台队列 ("thread") 中启动构建器到 运行 不阻塞主线程是正确的。完成。

切换回主线程是正确的,如果你想用 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

你可以想象,当你拿到最后一项时,if 可以在你读取它的下一瞬间从数组中移除。也许这会导致您的代码出现不一致的问题。这真的取决于你的代码。

编辑结束

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里面也要这样做。