Skip to content

Concurrency: Factor atomic operations in Task.sleep() into a Sendable wrapper #72062

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 5, 2024

Conversation

tshortli
Copy link
Contributor

@tshortli tshortli commented Mar 4, 2024

When building the Swift standard library, the compiler warns:

warning: capture of 'wordPtr' with non-sendable type 'UnsafeMutablePointer<Builtin.Word>' in a `@Sendable` closure

This diagnostic will become an error in the Swift 6 language mode, so it needs to be addressed. This PR factors the atomic operations used by the implementation of Task.sleep() into an @unchecked Sendable wrapper in order to convince the compiler that these operations are thread-safe. As an added benefit, the code is more readable when the atomic operations are abstracted away. This refactor intentionally translates the existing implementation into a wrapper as faithfully as possible and does not attempt to improve on any other aspects of the implementation, such as the unsafe manual memory allocation and deallocation.

… wrapper.

When building the Swift standard library, the compiler warns:

```
warning: capture of 'wordPtr' with non-sendable type 'UnsafeMutablePointer<Builtin.Word>' in a `@Sendable` closure
```

This diagnostic will become an error in the Swift 6 language mode, so it needs
to be addressed. This PR factors the atomic operations used by the
implementation of `Task.sleep()` into an `@unchecked Sendable` wrapper in order
to convince the compiler that these operations are thread-safe. As an added
benefit, the code is more readable when the atomic operatios are abstracted
away. This refactor intentionally translates the existing implementation into a
wrapper as faithfully as possible and does not attempt to improve on any other
aspects of the implementation, such as the unsafe manual memory allocation and
deallocation.
@tshortli tshortli requested review from DougGregor and hborla March 4, 2024 17:34
@tshortli tshortli requested a review from ktoso as a code owner March 4, 2024 17:34
@tshortli
Copy link
Contributor Author

tshortli commented Mar 4, 2024

@swift-ci please test

@Azoy
Copy link
Contributor

Azoy commented Mar 4, 2024

I wonder if we can use Atomic here? Would require making some things noncopyable, but it should be fine? I haven't taken look to see if we'd eventually end up in some ABI type (I stopped at SleepState which is non-ABI). Alternatively, we're close to having pointers to noncopyable types so we could say UnsafeMutablePointer<Atomic<Int>> soon. (Why is this using Builtin.Word instead of Int?)

@tshortli
Copy link
Contributor Author

tshortli commented Mar 4, 2024

I wonder if we can use Atomic here? Would require making some things noncopyable, but it should be fine? I haven't taken look to see if we'd eventually end up in some ABI type (I stopped at SleepState which is non-ABI). Alternatively, we're close to having pointers to noncopyable types so we could say UnsafeMutablePointer<Atomic<Int>> soon.

When working on this I did briefly try to explore whether it was possible to use a non-copyable type to entirely replace the need for a pointer to heap allocated storage. However, as far as I can tell there'd be no way to convince the compiler that the lifetime of the closure passed to Builtin.createAsyncTask that captures the "token" does not exceed the lifetime of a value stored on the current task's stack because that closure is necessarily @escaping.

If adding a dependency on Synchronization to _Concurrency is okay, then preferring UnsafeMutablePointer<Atomic<Int>> over using built-ins seems reasonable to me. Everything changed in this PR is an implementation detail and shouldn't affect ABI. That said, I don't want to block the progress this PR makes on improving what's there today, so if we can't adopt Atomic quite yet then I'd prefer to get a reasonable version of this that's a low-risk adaptation of the original code landed. We can follow up to replace the use of UnsafeMutablePointer<Builtin.Word> and the built-ins in a subsequent PR.

(Why is this using Builtin.Word instead of Int?)

To match the expectations of the existing builtins used for atomic operations.

@tshortli
Copy link
Contributor Author

tshortli commented Mar 4, 2024

When working on this I did briefly try to explore whether it was possible to use a non-copyable type to entirely replace the need for a pointer to heap allocated storage. However, as far as I can tell there'd be no way to convince the compiler that the lifetime of the closure passed to Builtin.createAsyncTask that captures the "token" does not exceed the lifetime of a value stored on the current task's stack because that closure is necessarily @escaping.

Ok, after looking into more I think this should be doable by using withoutActuallyEscaping. I'm going to try to implement a version of this with Atomic and open that as a PR stacked on this one so we can consider it separately.

@tshortli
Copy link
Contributor Author

tshortli commented Mar 4, 2024

@Azoy I created a draft prototype of adopting Atomic instead here. As it turns out it works quite well, though I admittedly don't understand how the compiler is managing the lifetime of the Atomic state relative to the escaping closures which borrow it. The main blocker is that Atomic does not yet have real availability, so we can't actually use it yet in the implementation of _Concurrency.

@tshortli tshortli merged commit a983de3 into swiftlang:main Mar 5, 2024
@tshortli tshortli deleted the sendable-sleep-state branch March 5, 2024 16:28
while true {
let state = SleepState(loading: wordPtr)
let state = token.load()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

late to the party here -- I was looking into this recently and it turned out we didn't properly handle nonisolated(unsafe) for local variables (heh). That was fixed since then so I wondered if we could just nonisolated(unsafe) the wordPtr now and avoid the wrapper entirely.

Although avoiding the alloc entirely would be even better...! So thanks for investigating that too.

@ktoso
Copy link
Contributor

ktoso commented Mar 11, 2024

This resolved rdar://117594245

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants