Understanding Swift's value type thread safety

This really helps crystallize the mechanics of these scenarios. It's unfortunate how easy it is to (silently) lose the safety that value types provide when handing them off to an escaping closure. It makes sense that it behaves this way but the lack of explicit signalling required is a potential footgun. shared/__block would help but I wonder if something at the point of use/capture would be even better (similar to explicit self in a closure).

Is a mutation in an escaping closure required to trigger the storage change or will any access also cause the change? I'm experimenting with reading in the closure and mutating in the function scope and thread sanitizer complains.

    // No crash
    // Thread sanitizer: Swift access race on store.append("\(i)")
    func testScenarioD1() throws {
        let iterations = 1_000_000
        var store = "hello"
        DispatchQueue.global().async {
            for _ in 1...iterations {
                _ = store
            }
        }
        for i in 1...iterations {
            store.append("\(i)")
        }
    }