Array中的Swift线程问题

问题描述 投票:0回答:3

在我的项目中,有一个数据提供者,每2毫秒提供一次数据。以下是数据获取的委托方法。

func measurementUpdated(_ measurement: Double) {
    measurements.append(measurement)

    guard measurements.count >= 300 else { return }
    ecgView.measurements = Array(measurements.suffix(300))

    DispatchQueue.main.async {
        self.ecgView.setNeedsDisplay()
    }

    guard measurements.count >= 50000 else { return }

    let olderMeasurementsPrefix = measurements.count - 50000
    measurements = Array(measurements.dropFirst(olderMeasurementsPrefix))

    print("Measurement Count : \(measurements.count)")
}

我想要做的是,当数组有超过50000个元素时,删除Array的前n个索引中的旧测量,我正在使用Array的dropFirst方法。

但是,我收到了以下消息的崩溃:

致命错误:无法使用upperBound <lowerBound形成范围

我认为由于线程,包括附加和删除引起的问题可能同时发生,因为委托以2毫秒的时间间隔触发。你能建议我一个解决这个问题的优化方法吗?

arrays swift thread-safety
3个回答
3
投票

因此,要真正解决这个问题,我们需要首先解决您的两个主张:

1)你说实际上,在主线程上会调用measurementUpdated()(因为你说在主线程上都会调用append和dropFirst。你还说过几次,每隔2ms会调用一次measurementUpdated()。不希望在主线程上每隔2ms调用一个方法。你会很快堆积很多,并且在更新时会遇到很多延迟,因为主线程会有UI内容要做,这总是占用时间。

所以第一条规则:应始终在另一个线程上调用measurementUpdated()。但是,保持它是相同的线程。

第二条规则:从调用measurementUpdated()的任何收集数据的整个代码路径也必须在非主线程上。它可以在测量更新()的线程上,但不一定是。

第三条规则:您不需要每2毫秒更新一次UI图。人眼无法感知到比大约150毫秒更快的UI变化。此外,设备的主线程将完全陷入困境,尝试每2ms重新渲染一次。我敢打赌你的图形UI甚至无法在2ms内渲染一次!所以让我们给你的主线程一个中断,只需每隔150ms更新一次图形。测量MS中的当前时间,并与上次从此例程更新图表进行比较。

第四条规则:不要在不进行互斥锁的情况下更改两个不同线程中的任何数组(或任何对象),因为它们有时会发生冲突(一个线程将尝试对其执行操作,而另一个线程也是如此)。 Matt Gallagher的Mutexes and closure capture in Swift是一篇很好的文章,涵盖了当前所有快速执行互斥锁的方法。这是一个很好的阅读,并有简单和先进的解决方案和他们的权衡。

另一个建议:你每2ms分配或重新分配一些数组。我认为这是不必要的,并且会对引擎盖下的内存池造成不必要的压力。我建议不要追加和DropFirst调用。尝试重写,以便您拥有一个容纳50,000个双打的单个阵列,并且永远不会改变大小。只需更改数组中的值,并保留2个索引,以便始终知道数据集的“开始”和“结束”在数组中的位置。即在最后一个是第一个数组元素之后假装下一个数组元素(假装数组循环到前面)。然后你根本就没有翻腾记忆,而且它的运行速度也会更快。您肯定可以找到人们编写的数组扩展,以便使用这些扩展。每隔150毫秒,您可以按正确的顺序将数据复制到第二个预分配的数组中,以供图形UI使用,或者如果您拥有图形UI并且可以调整它以适应,则只需将两个索引传递给图形UI。

我现在没有时间编写一个涵盖所有这些内容的代码示例(可能是别人做的),但我明天会尝试重新审视。如果你自己重新尝试了它,那么对你来说实际上要好得多,然后如果你遇到困难就问我们一个新的问题(在一个新的StackOverflow上)。


0
投票

根据你的说法,我将假设委托从并发线程调用measuramentUpdate方法。

如果是这种情况,并且问题确实与线程有关,那么这应该可以解决您的问题:

func measurementUpdated(_ measurement: Double) {
    DispatchQueue(label: "MySerialQueue").async {
        measurements.append(measurement)

        guard measurements.count >= 300 else { return }
        ecgView.measurements = Array(measurements.suffix(300))

        DispatchQueue.main.async {
            self.ecgView.setNeedsDisplay()
        }

        guard measurements.count >= 50000 else { return }

        let olderMeasurementsPrefix = measurements.count - 50000
        measurements = Array(measurements.dropFirst(olderMeasurementsPrefix))

        print("Measurement Count : \(measurements.count)")
    }
}

这将把代码放在一个串行队列中。这样,您可以确保此代码块一次只运行一个。


0
投票

更新为@Smartcat正确指出,如果主线程速度不足以以与工作线程生成它们相同的速度消耗数组,则此解决方案可能会导致内存问题。


问题似乎是由ecgViewmeasurements属性引起的:你在接收数据的线程上写入它,而视图试图在主线程上读取它,并且(不幸的是)同时从多个线程访问相同的数据可能会产生竞争条件。

总之,您需要确保读取和写入都发生在同一个线程上,并且可以轻松实现我在异步调度中移动setter调用:

let ecgViewMeasurements = Array(measurements.suffix(300))

DispatchQueue.main.async {
    self.ecgView.measurements = ecgViewMeasurements
    self.ecgView.setNeedsDisplay()
}
© www.soinside.com 2019 - 2024. All rights reserved.