Analysing my code with the new exciting Thread Sanitizer tool from Xcode 8, in certain scenarios I'm experiencing unexpected data races in `deinit` of classes which protect the access to their properties with a synchronization primitive (a dispatch queue) .
Given a class, with a "sync queue" and a synchronized member value:
enum MyEnum {
case first(_: String)
case second(_: [String])
}
public class Foo {
private let _syncQueue = DispatchQueue(label: "foo-sync-queue", attributes: .serial)
private var _value: MyEnum = .first("")
public init() {
}
deinit {
}
public func modify(value: String) {
_syncQueue.async {
self._value = .first(value)
}
}
}
What's not immediately obvious is the fact, that the deinit method must access the enum in order to figure out how to deallocate the underlying value.
Suppose, method `modify` will be executed on Thread A. This causes the actual write operation to be executed on a thread, say M, which is synchronized with the `syncQueue`.
It may happen that Thread A equals Thread M, but that is random.
Now, suppose a reference to variable of type `Foo` exists in Thread B. It happens that this is the last strong reference to the variable. Now, when this variable ceases to exist, the `deinit` method of the variable will be called. This deinit reads from the memory store for member `_value`. This read operation is not synchronized with the sync queue. Since the executing thread happens to be not equal the thread M, where the previous write operation has been performed, a data race occurs.
This data race is not easy to fix. I dare to state, it is even impossible in certain use cases. For example:
Suppose there is a function which returns a new value of type Foo:
func createFoo() -> Foo
And then:
createFoo().modify(value: "Hello Data Race")
The problem here is, that the live-time of the returned temporary is largely uncontrollable by the programmer. This prevents the programmer to determine _when_ the variable foo should be deallocated. And to be honest, a programmer shouldn't care about these details anyway!
In practice, this creates a very reliable data race.
This use case is not artificial in the functional paradigm. Actually, I'm having this issue, and I have no idea how to fix it - except there would be help with a corresponding language construct (e.g. let us implement the deinit function) - which does not currently exists.
Any ideas are appreciated.