I have similar concerns.
One way to make a type Sendable is indeed to protect its internal state with a lock. This is one use cases of the recent [Pitch] Synchronous Mutual Exclusion Lock (cc @Alejandro).
I have also tried to make my mutex a property wrapper, hoping that the language would show some awareness for well-behaved programs. But it does not work very well:
@Mutex var value = 1
run { // an @escaping @Sendable closure
// Error: Mutation of captured var 'value'
// in concurrently-executing code
value = 2
// OK
$value.withLock { $0 = 2 }
}
run {
print(
}
I use this a lot in tests of asynchronous-but-not-async methods (similar to DispatchQueue.async):
@MainActor // for waitForExpectations
func test_that_value_is_2() {
let expectation = self.expectation(description: "")
@Mutex var value: Int? = nil
giveMeTwoEventually { n in // an @escaping @Sendable closure
$value.withLock { $0 = n }
expectation.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(value, 2)
}
Without strict concurrency checks, the tests is easier to write, obviously:
// Without strict checks
func test_that_value_is_2() {
let expectation = self.expectation(description: "")
var value: Int?
giveMeTwoEventually { n in
value = n
expectation.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(value, 2)
}