-
Notifications
You must be signed in to change notification settings - Fork 10.5k
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
Conversation
406cf71
to
f91bfb3
Compare
b256a70
to
5b8d0e9
Compare
@swift-ci Please test |
Build failed |
5b8d0e9
to
d53522f
Compare
@swift-ci Please test |
d53522f
to
050f511
Compare
@swift-ci Please test |
Build failed |
Build failed |
050f511
to
1fccb4b
Compare
@swift-ci Please test |
Build failed |
Build failed |
1fccb4b
to
2ab4829
Compare
@swift-ci Please test |
Build failed |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Int8PtrTy | |
Int8PtrTy // resultBuffer |
?
HeapObject *closureContext, | ||
void *resultBuffer) { | ||
auto flags = TaskCreateFlags(); | ||
flags.setEnqueueJob(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also setIsChildTask
I think...?
There was a problem hiding this comment.
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); | ||
} |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 👍
2ab4829
to
a1df816
Compare
@swift-ci Please test |
Build failed |
Build failed |
@swift-ci Please test |
Build failed |
Build failed |
@swift-ci Please test Linux |
Build failed |
2a9f5c6
to
eb587fb
Compare
@swift-ci Please test Linux |
Build failed |
@swift-ci Please test Linux |
Build failed |
c1242e3
to
2fb271e
Compare
Please test with following PR: @swift-ci test linux |
Please test with following PR: @swift-ci test macOS |
Build failed |
Build failed |
2fb271e
to
7cb9c43
Compare
Please test with following PR: @swift-ci Please test |
Build failed |
7cb9c43
to
90b7cb9
Compare
Please test with following PR: @swift-ci Please test |
Build failed |
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
90b7cb9
to
7470155
Compare
Please test with following PR: @swift-ci Please test |
Build failed |
@swift-ci Please test OS X |
Testing if it tests cleanly with all of the other memory bugs fixed, but without the LLVM patch: |
@swift-ci Please test |
1 similar comment
@swift-ci Please test |
Change the code generation patterns for
async let
bindings to use an ABI based on the followingfunctions:
swift_asyncLet_start
, which as before, starts anasync let
child task, but which additionallynow 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 replacesswift_asyncLet_wait[_throwing]
. Instead ofreturning 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 initializedvalue.
swift_asyncLet_finish
, which replacesswift_asyncLet_end
. Unlike_end
, this variantis 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 necessaryas 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 anasync let
variablemore than once.
rdar://77855176