I am trying to understand what’t wrong in my approach of access to array. In my app I got several not reproducible crashes with EXC_BAD_ACCESS KERN_INVALID_ADDRESS (some not 0 address)
. I suggested that it was related to read/write access to array. To investigate I wrote code a bit similar to the app code for playground:
import Foundation
import CoreBluetooth
let requiredServices: [CBUUID] = [
CBUUID(string: "180A"),
CBUUID(string: "280A"),
CBUUID(string: "380A"),
CBUUID(string: "480A"),
CBUUID(string: "580A"),
CBUUID(string: "680A"),
CBUUID(string: "780A"),
CBUUID(string: "880A"),
]
// Array to investigate
var availableServices = [CBUUID]()
let queues = [
DispatchQueue(label: "test0"),
DispatchQueue(label: "test1"),
DispatchQueue(label: "test2"),
DispatchQueue(label: "test3"),
DispatchQueue(label: "test4"),
DispatchQueue(label: "test5"),
DispatchQueue(label: "test6"),
DispatchQueue(label: "test7"),
DispatchQueue(label: "test8"),
DispatchQueue(label: "test9"),
DispatchQueue(label: "test10")
]
func mainTest() {
print(">>>> Main test")
for i in 0...100 {
for j in 1...10 {
queues[0].async {
for _ in 1...100 {
// Write operation
doAppendAndClear()
}
}
queues[j].async {
for k in 1...100 {
// Read operation
tryToRead()
if i == 100, j == 10, k == 100 {
print(">>>> The end")
}
}
}
}
}
}
func doAppendAndClear() {
availableServices.append(CBUUID(string: "180A"))
availableServices.append(CBUUID(string: "280A"))
availableServices = []
availableServices.append(CBUUID(string: "380A"))
availableServices.append(CBUUID(string: "480A"))
availableServices = []
availableServices.append(CBUUID(string: "580A"))
availableServices.append(CBUUID(string: "680A"))
availableServices = []
availableServices.append(CBUUID(string: "780A"))
availableServices.append(CBUUID(string: "880A"))
availableServices = []
}
func tryToRead() {
let services = availableServices
var available = true
requiredServices.forEach({ available = available && services.contains($0)})
}
mainTest()
I got this warninig in playground and the same warning when I pasted the test in app code:
Object 0x600000e631e0 of class _ContiguousArrayStorage deallocated with non-zero retain count 2. This object’s deinit, or something called from it, may have created a strong reference to self which outlived deinit, resulting in a dangling reference
Besides that I was getting crashes in the app when I pasted that tested code there. It happened in tryToRead()
method, in the last line of it:
thread #17, queue = 'test5', stop reason = EXC_BAD_ACCESS (code=1, address=0x97b30d800)
frame #0: 0x000000018c3f2244 libobjc.A.dylib`objc_retain_x8 + 16
frame #1: 0x0000000193172210 libswiftCore.dylib`swift::metadataimpl::ValueWitnesses<swift::metadataimpl::ObjCRetainableBox>::initializeWithCopy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) + 28
frame #2: 0x0000000192ddf844 libswiftCore.dylib`Swift.Array.subscript.read : (Swift.Int) -> τ_0_0 + 232
frame #3: 0x0000000192ddf728 libswiftCore.dylib`protocol witness for Swift.Collection.subscript.read : (τ_0_0.Index) -> τ_0_0.Element in conformance Swift.Array<τ_0_0> : Swift.Collection in Swift + 68
frame #4: 0x0000000192e3fad0 libswiftCore.dylib`protocol witness for Swift.IteratorProtocol.next() -> Swift.Optional<τ_0_0.Element> in conformance Swift.IndexingIterator<τ_0_0> : Swift.IteratorProtocol in Swift + 676
frame #5: 0x0000000192f88164 libswiftCore.dylib`Swift.Sequence.contains(where: (τ_0_0.Element) throws -> Swift.Bool) throws -> Swift.Bool + 664
frame #6: 0x0000000192e767ec libswiftCore.dylib`Swift.Sequence< where τ_0_0.Element: Swift.Equatable>.contains(τ_0_0.Element) -> Swift.Bool + 156
* frame #7: 0x0000000104d2a3d4 MyApp`closure #1 in ContainerVC.tryToRead($0=0x0000000303fde400, available=true, services=2 values) at ContainerVC.swift:202:58
frame #8: 0x0000000104d310d4 MyApp`partial apply for closure #1 in ContainerVC.tryToRead() at <compiler-generated>:0
frame #9: 0x0000000192ea2e70 libswiftCore.dylib`Swift.Sequence.forEach((τ_0_0.Element) throws -> ()) throws -> () + 756
frame #10: 0x0000000104d2a2dc MyApp`ContainerVC.tryToRead(self=0x00000001420c7800) at ContainerVC.swift:202:26
frame #11: 0x0000000104d29c2c MyApp`closure #2 in ContainerVC.mainTest(self=0x00000001420c7800, i=0, j=5) at ContainerVC.swift:166:30
Original crash in firebase has different top lines in stack:
Crashed: com.apple.main-thread
0 libswiftCore.dylib 0x2f7178 _swift_release_dealloc + 16
1 libswiftCore.dylib 0x2f8060 bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1> >::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 184
So, the question is why that dangling pointes appeared? Also maybe you know the best practices to avoid that when perform read/write to array?
2
Answers
In short, the issue arises when one thread is trying to copy data from an array
the operation might stop in between, because it's not atomic. Then another thread can change
availableServices
:Then when the first thread will continue it will get corrupted data.
To avoid this we should implement access to the array in a thread-safe way.
This stack trace, the “dangling pointer” reference, etc., is a red herring. This code is simply not thread-safe. If you do this within Xcode, that offers additional diagnostic tools, notably the Thread Sanitizer (aka TSAN) . If you use TSAN, it will report “Swift access race”, i.e., the thread-safety violation:
You must synchronize your access to the array in order to ensure thread safety. All interaction must be synchronized, using a mechanism, such as an actor, a lock, or a serial dispatch queue, amongst others. You cannot have any access (whether reads or writes) while a write is underway.
For information on how we do this with actors, see WWDC 2022 video, Eliminate data races using Swift Concurrency
At the risk of going into the weeds, let me address a comment you made:
The problem is that you are not dealing with a copy of
array
. Consider this simplified (!) example:If you do this within Xcode, it will warn you that it is not
array
that is captured, but ratherself
!This is the hint regarding the thread-safety issues (shown in Xcode, but not necessarily playgrounds or the like). To resolve that warning, we can add the explicit
self
references:Fine, that eliminates the warnings, but this results in the following behavior, where you can see that
array
changed on another thread (!):And if we turn on TSAN, we will receive an error, informing us of the thread-safety violation:
To fix this, we must change the
read
operation to explicitly fetch the value and then use that copy of the array in read operation. E.g., you could use a capture list to make a copy of thearray
:Or you can manually make your own copy:
In both of these examples, we explicitly use the
synchronizationQueue
to get a copy of the array. Either way, you will now see the copy of thearray
will not change while the original array mutated on the other thread. And there will no longer be a warning from TSAN.I confess that this example is a little contrived, but I wanted to illustrate that you do not automatically get a copy of the array and we have to do so manually. There are lots of other ways to make copies of the array, but hopefully this illustrates the underlying problem.