Skip to content

Handle multiple awaits and suspend-on-exit for async let tasks. #37938

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 2 commits into from
Jul 23, 2021

Conversation

jckarter
Copy link
Contributor

@jckarter jckarter commented Jun 16, 2021

Change the code generation patterns for async let bindings to use an ABI based on the following
functions:

  • swift_asyncLet_start, which as before, starts an async let child task, but which additionally
    now associates the async let with a caller-owned buffer to receive the result of the task.
    This is intended to allow the task to emplace its result in caller-owned memory, allowing the
    child task to be deallocated after completion without invalidating the result buffer.
  • swift_asyncLet_get[_throwing], which replaces swift_asyncLet_wait[_throwing]. Instead of
    returning a copy of the value, this entry point concerns itself with populating the local buffer.
    If the buffer hasn't been populated, then it awaits completion of the task and emplaces the
    result in the buffer; otherwise, it simply returns. The caller can then read the result out of
    its owned memory. These entry points are intended to be used before every read from the
    async let binding, after which point the local buffer is guaranteed to contain an initialized
    value.
  • swift_asyncLet_finish, which replaces swift_asyncLet_end. Unlike _end, this variant
    is async and will suspend the parent task after cancelling the child to ensure it finishes
    before cleaning up. The local buffer will also be deinitialized if necessary. This is intended
    to be used on exit from an async let scope, to handle cleaning up the local buffer if necessary
    as well as cancelling, awaiting, and deallocating the child task.

To avoid an epoch break with existing swiftinterfaces and ABI clients, the old builtins and entry
points are kept intact for now, but SILGen now only generates code using the new interface.

This new interface fixes several issues with the old async let codegen, including use-after-free
crashes if the async let was never awaited, and the inability to read from an async let variable
more than once.

rdar://77855176

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 406cf71 to f91bfb3 Compare June 16, 2021 01:16
@jckarter jckarter force-pushed the async-let-multi-suspend branch 2 times, most recently from b256a70 to 5b8d0e9 Compare July 7, 2021 23:38
@jckarter jckarter requested review from ktoso, DougGregor and rjmccall July 7, 2021 23:39
@jckarter jckarter changed the title [WIP] Revise async let ABI to efficiently allow multiple reads. Handle multiple awaits and suspend-on-exit for async let tasks. Jul 7, 2021
@jckarter jckarter marked this pull request as ready for review July 7, 2021 23:39
@jckarter
Copy link
Contributor Author

jckarter commented Jul 7, 2021

@swift-ci Please test

@swift-ci
Copy link
Contributor

swift-ci commented Jul 8, 2021

Build failed
Swift Test Linux Platform
Git Sha - 5b8d0e98339c46cf541e83995e5cf23ef15ce196

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 5b8d0e9 to d53522f Compare July 8, 2021 20:09
@jckarter
Copy link
Contributor Author

jckarter commented Jul 8, 2021

@swift-ci Please test

@jckarter jckarter force-pushed the async-let-multi-suspend branch from d53522f to 050f511 Compare July 9, 2021 21:10
@jckarter
Copy link
Contributor Author

jckarter commented Jul 9, 2021

@swift-ci Please test

@swift-ci
Copy link
Contributor

swift-ci commented Jul 9, 2021

Build failed
Swift Test Linux Platform
Git Sha - 050f511e140febf6be66289f46bb0fc6b8a4f929

@swift-ci
Copy link
Contributor

swift-ci commented Jul 9, 2021

Build failed
Swift Test OS X Platform
Git Sha - 050f511e140febf6be66289f46bb0fc6b8a4f929

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 050f511 to 1fccb4b Compare July 9, 2021 23:16
@jckarter
Copy link
Contributor Author

jckarter commented Jul 9, 2021

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 1fccb4bb6cfe5b2f7dd51a56d772464422c65dd4

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 1fccb4bb6cfe5b2f7dd51a56d772464422c65dd4

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 1fccb4b to 2ab4829 Compare July 10, 2021 01:14
@jckarter
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 2ab4829e35b7f1d4553487c3faa23cc50e95113d

Copy link
Contributor

@ktoso ktoso left a comment

Choose a reason for hiding this comment

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

Read through in depth and looks fantastic as far as I can tell :-)

/// The number of words in an AsyncLet (flags + task pointer)
NumWords_AsyncLet = 8, // TODO: not sure how much is enough, these likely could be pretty small
/// The number of words in an AsyncLet (flags + child task context & allocation)
NumWords_AsyncLet = 80, // 640 bytes ought to be enough for anyone
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why so much, is it because the task context and place where we'll store the result basically ends up statically as large as the result struct as well, so we effectively have a lot of bytes to use without additional alloc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use the same block for the child task's initial slab, and the previous code gave the child 512 bytes of slab to begin with. 640 seemed like a nice round number to allow for some growth in the task context header overhead and still ensure at least 512 bytes still remain for the child context to start with.

void *getPreallocatedSpace();

/// Return the size of the unused space within the async let block.
static size_t getSizeOfPreallocatedSpace();
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

@@ -36,15 +36,23 @@ class alignas(Alignment_AsyncLet) AsyncLet {
constexpr AsyncLet()
: PrivateData{} {}

// FIXME: not sure how many words we should reserve
void *PrivateData[NumWords_AsyncLet];

// TODO: we could offer a "was awaited on" check here
Copy link
Contributor

Choose a reason for hiding this comment

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

This is solved now by this PR! :-)

specifically by: llvm::PointerIntPair<AsyncTask *, 1, bool> taskAndHasResult; that boolean is exactly the flag this was alluding to.

///
/// \code
/// func swift_asyncLet_finish(_ asyncLet: Builtin.RawPointer,
/// _ resultBuffer: Builtin.RawPointer) async
Copy link
Contributor

Choose a reason for hiding this comment

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

minor nitpick: use same formatting as other docs? (cut down at the parameters)

ARGS(SwiftAsyncLetPtrTy, // AsyncLet*
SwiftTaskOptionRecordPtrTy, // options
TypeMetadataPtrTy, // futureResultType
Int8PtrTy, // closureEntry
OpaquePtrTy, // closureContext
Int8PtrTy
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Int8PtrTy
Int8PtrTy // resultBuffer

?

HeapObject *closureContext,
void *resultBuffer) {
auto flags = TaskCreateFlags();
flags.setEnqueueJob(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Also setIsChildTask I think...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, I followed the code that asyncLet_start already had for getting an async let task started. Is that not implied by being an async let task?

// Don't need to do anything if the result buffer is already populated.
if (asImpl(alet)->hasResultInBuffer()) {
return resumeFunction(callerContext);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

asImpl(alet)->setHasResultInBuffer();
swift_task_future_wait(reinterpret_cast<OpaqueValue*>(resultBuffer),
callerContext, alet->getTask(),
resumeFunction, callContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

Right, that should be safe to do 👍

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 2ab4829 to a1df816 Compare July 12, 2021 17:23
@jckarter
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - a1df8165a42daa79587a70d6bb854c2292793b42

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - a1df8165a42daa79587a70d6bb854c2292793b42

@jckarter
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 8101b02c6d0a13ba390bb545d1404fac8c8b8730

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - b8100389c4e9b825473d106e5120319b4b03c580

@jckarter
Copy link
Contributor Author

@swift-ci Please test Linux

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 2a9f5c6f27e417ad2189e6735e0d3c20d7d71fb4

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 2a9f5c6 to eb587fb Compare July 16, 2021 16:05
@jckarter
Copy link
Contributor Author

@swift-ci Please test Linux

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - eb587fbe1de48fb9e62a95f6529efa611d742bb7

@jckarter
Copy link
Contributor Author

@swift-ci Please test Linux

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - d68b80d5968d2e932c064407d13c7960673385c7

@jckarter jckarter force-pushed the async-let-multi-suspend branch from c1242e3 to 2fb271e Compare July 19, 2021 22:07
@aschwaighofer
Copy link
Contributor

Please test with following PR:
swiftlang/llvm-project#3121

@swift-ci test linux

@aschwaighofer
Copy link
Contributor

Please test with following PR:
swiftlang/llvm-project#3121

@swift-ci test macOS

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 2fb271eb37010838704918624e44807b330d8073

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 2fb271eb37010838704918624e44807b330d8073

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 2fb271e to 7cb9c43 Compare July 21, 2021 20:09
@jckarter
Copy link
Contributor Author

Please test with following PR:
swiftlang/llvm-project#3121

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 7cb9c432233354283eb7e442561c41eaacc9bac8

@jckarter jckarter force-pushed the async-let-multi-suspend branch from 7cb9c43 to 90b7cb9 Compare July 21, 2021 23:23
@jckarter
Copy link
Contributor Author

Please test with following PR:
swiftlang/llvm-project#3121

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 90b7cb9902c1709624facedc79d5d8f4a7529e0d

jckarter added 2 commits July 22, 2021 10:19
Change the code generation patterns for `async let` bindings to use an ABI based on the following
functions:

- `swift_asyncLet_begin`, which starts an `async let` child task, but which additionally
  now associates the `async let` with a caller-owned buffer to receive the result of the task.
  This is intended to allow the task to emplace its result in caller-owned memory, allowing the
  child task to be deallocated after completion without invalidating the result buffer.
- `swift_asyncLet_get[_throwing]`, which replaces `swift_asyncLet_wait[_throwing]`. Instead of
  returning a copy of the value, this entry point concerns itself with populating the local buffer.
  If the buffer hasn't been populated, then it awaits completion of the task and emplaces the
  result in the buffer; otherwise, it simply returns. The caller can then read the result out of
  its owned memory. These entry points are intended to be used before every read from the
  `async let` binding, after which point the local buffer is guaranteed to contain an initialized
  value.
- `swift_asyncLet_finish`, which replaces `swift_asyncLet_end`. Unlike `_end`, this variant
  is async and will suspend the parent task after cancelling the child to ensure it finishes
  before cleaning up. The local buffer will also be deinitialized if necessary. This is intended
  to be used on exit from an `async let` scope, to handle cleaning up the local buffer if necessary
  as well as cancelling, awaiting, and deallocating the child task.
- `swift_asyncLet_consume[_throwing]`, which combines `get` and `finish`. This will await completion
  of the task, leaving the result value in the result buffer (or propagating the error, if it
  throws), while destroying and deallocating the child task. This is intended as an optimization
  for reading `async let` variables that are read exactly once by their parent task.

To avoid an epoch break with existing swiftinterfaces and ABI clients, the old builtins and entry
points are kept intact for now, but SILGen now only generates code using the new interface.

This new interface fixes several issues with the old async let codegen, including use-after-free
crashes if the `async let` was never awaited, and the inability to read from an `async let` variable
more than once.

rdar://77855176
This isn't implemented. It would never make sense for escaping captures of
`async let` values, because doing so would either involve an implicit awaiting
of the value on capture, or else stretching the lifetime of the task past its
scope. In nonescaping contexts, and in nested `async let` expressions, it might
be implementable in the future. rdar://80043610
@jckarter jckarter force-pushed the async-let-multi-suspend branch from 90b7cb9 to 7470155 Compare July 22, 2021 17:19
@jckarter
Copy link
Contributor Author

Please test with following PR:
swiftlang/llvm-project#3121

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 7470155

@jckarter
Copy link
Contributor Author

@swift-ci Please test OS X

@jckarter
Copy link
Contributor Author

Testing if it tests cleanly with all of the other memory bugs fixed, but without the LLVM patch:

@jckarter
Copy link
Contributor Author

@swift-ci Please test

1 similar comment
@jckarter
Copy link
Contributor Author

@swift-ci Please test

@jckarter jckarter merged commit fc67ba5 into swiftlang:main Jul 23, 2021
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.

5 participants