Skip to content

[Concurrency] Implement basic runtime support for task futures. #34703

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 13 commits into from
Nov 15, 2020

Conversation

DougGregor
Copy link
Member

Extend AsyncTask and the concurrency runtime with basic support for
task futures. AsyncTasks with futures contain a future fragment with
information about the type produced by the future, and where the
future will put the result value or the thrown error in the initial
context.

We still don't have the ability to schedule the waiting tasks on an
executor when the future completes, so this isn't useful for anything
just test, and we can only test limited code paths.

/// wait queue and will be scheduled when the future completes or
/// is cancelled. Otherwise, the future has completed and can be
/// queried.
FutureFragment::Status waitFuture(AsyncTask *waitingTask);
Copy link
Contributor

Choose a reason for hiding this comment

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

is this more like pollFuture or peekFutureStatus/peekStatus?
wait to me implies waiting but we don't really do that, just check what we should do next

Copy link
Member Author

Choose a reason for hiding this comment

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

If the future is still executing, it puts waitingTask on the queue to be scheduled once the future has completed. We then get to yield the thread back to the scheduler to do more work (but don't have the way to do that yet)

auto storagePtr = fragment->getStoragePtr();

// Check for an error.
if (unsigned errorOffset = fragment->errorOffset) {
Copy link
Contributor

Choose a reason for hiding this comment

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

for my understanding... when would this be 0? or just general pattern to avoid a nullptr if somehow(?) it would happen?

Copy link
Member Author

Choose a reason for hiding this comment

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

It could be zero for a non-throwing function. We should probably turn that into a JobFlag.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I see, thanks! Yeah may be a job flag would be nicer.

@DougGregor
Copy link
Member Author

@swift-ci please smoke test

unsigned resultOffset;

/// The offset of the error in the initial asynchronous context.
unsigned errorOffset;
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the purpose of these?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is me not knowing how to get a Swift function to put result values into the future.

break;

case Status::Error:
swift_unknownObjectRelease(getStoragePtr());
Copy link
Contributor

Choose a reason for hiding this comment

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

Please do not release an interior pointer into the future fragment. :)

Also, please use swift_errorRelease.

Copy link
Member Author

Choose a reason for hiding this comment

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

swift_errorRelease is only defined when Objective-C interoperability is enabled. But yeah, the other thing is bad ;)

while (true) {
waitingTask->NextWaitingTask = fragment->waitQueue.load();
if (fragment->waitQueue.compare_exchange_strong(
waitingTask->NextWaitingTask, waitingTask)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The reason you should unify the two atomic fields is that you have a race here against someone concurrently completing the task. The thread that completes the task should exchange the appropriate completion value into the queue and then process the waiting tasks they just loaded (via the exchange). Here you need to loop trying to add the task to the queue until you see success.

Always use memory orderings when working with std::atomic. This needs to be a std::memory_order_release on success; it can be std::memory_order_relaxed on failure, except actually it needs to be std::memory_order_acquire because you're going to check for success/error.

Don't repeatedly reload waitQueue. Load it (with an acquire) into a local, then use that local in the compare_exchange. When you see a pointer, copy it into NextWaitingTask (or really the SchedulerPrivate contents).

You should use compare_exchange_weak since you're already doing this in a loop.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you! I've made all of these changes together. It looks much more reasonable now.

auto resultPtr = reinterpret_cast<OpaqueValue *>(
reinterpret_cast<char *>(context) + fragment->resultOffset);
fragment->resultType->vw_initializeWithTake(storagePtr, resultPtr);
fragment->status = FutureFragment::Status::Success;
Copy link
Contributor

Choose a reason for hiding this comment

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

The general ABI for indirect return values is that the address to fill would be passed as an argument, not that we'd make space for it in the initial context and that move out of that into the future.

I don't remember what convention we decided on for errors, but I doubt this it's this strange. Should there be a flag in the task reporting whether it can throw?

Copy link
Contributor

@rjmccall rjmccall left a comment

Choose a reason for hiding this comment

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

Thanks, looking a lot better.

@DougGregor DougGregor marked this pull request as ready for review November 13, 2020 20:19
@DougGregor
Copy link
Member Author

@swift-ci please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - ca83686ac2ad833f7bce039cf5f5396b2acd323a

@DougGregor
Copy link
Member Author

@swift-ci please smoke test

/// fragment.
static size_t storageOffset(const Metadata *resultType) {
size_t offset = sizeof(FutureFragment);
size_t alignment = std::max(resultType->vw_alignment(), alignof(void *));
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this max about? The error value? Could you use SwiftError* to communicate that, here and below?

...should I overlook that this is subtly pessimal in really implausible cases, like a highly-aligned byte? I'm not sure it can practically happen.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it's the SwiftError*. I can use that.

///
/// The low bits contain the status, the rest of the pointer is the
/// AsyncTask.
std::atomic<WaitQueueItem> waitQueue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically I don't think that tasks need to coincide with any status but Executing, so the other statuses could be signal values rather than masked on. I don't think it really matters, though.

break;

case Status::Error:
swift_unknownObjectRelease(reinterpret_cast<OpaqueValue *>(getError()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the standard way we release errors in the runtime? Shouldn't we have an swift_errorRelease we can unconditionally call, even if it just compiles to swift_release in some configurations? Is it not important that we call swift_errorRelease in the configurations that provide it?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is the standard way when we don't know whether we'll have a native Swift object or not. swift_errorRelease is implemented as objc_release where it's implemented.

auto nextWaitingTask = waitingTask->getNextWaitingTask();

// Remove this task from the list.
waitingTask->getNextWaitingTask() = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

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

You actually don't have to do this; the memory is invalidated once it's not waiting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alright.

JobFlags flags, AsyncTask *parent, const Metadata *futureResultType,
AsyncFunctionType<void()> *function, size_t initialContextSize) {
assert((futureResultType != nullptr) == flags.task_isFuture());
assert((futureResultType != nullptr) == flags.task_isFuture());
Copy link
Contributor

Choose a reason for hiding this comment

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

Seeing undouble?

Should we assert here that initialContextSize >= sizeof(FutureAsyncContext)?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a good idea!

/// task.
///
/// This type matches the ABI of a function `<T> () async throws -> T`, which
/// is used to describe futures.
Copy link
Contributor

Choose a reason for hiding this comment

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

To be precise: it's the signature of the function taken by runDetached and Task.Group.add. We basically fully set up the call except for any formal arguments (e.g. closure-context arguments).

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure.

@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor DougGregor force-pushed the concurrency-task-future branch from 1b7fdea to 5d38e0c Compare November 14, 2020 06:00
@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor
Copy link
Member Author

This makes no sense...

FAILED: unittests/runtime/SwiftRuntimeTests.exe 
cmd.exe /C "cd . && "C:\Program Files\CMake\bin\cmake.exe" -E vs_link_exe --intdir=unittests\runtime\CMakeFiles\SwiftRuntimeTests.dir --rc=C:\PROGRA~2\WI3CF2~1\10\bin\100183~1.0\x64\rc.exe --mt=C:\PROGRA~2\WI3CF2~1\10\bin\100183~1.0\x64\mt.exe --manifests  -- C:\PROGRA~2\MICROS~1\2019\COMMUN~1\VC\Tools\MSVC\1424~1.283\bin\Hostx64\x64\link.exe /nologo @CMakeFiles\SwiftRuntimeTests.rsp  /out:unittests\runtime\SwiftRuntimeTests.exe /implib:unittests\runtime\SwiftRuntimeTests.lib /pdb:unittests\runtime\SwiftRuntimeTests.pdb /version:0.0  /INCREMENTAL:NO /STACK:10000000 /INCREMENTAL:NO /subsystem:console  && cd ."
LINK: command "C:\PROGRA~2\MICROS~1\2019\COMMUN~1\VC\Tools\MSVC\1424~1.283\bin\Hostx64\x64\link.exe /nologo @CMakeFiles\SwiftRuntimeTests.rsp /out:unittests\runtime\SwiftRuntimeTests.exe /implib:unittests\runtime\SwiftRuntimeTests.lib /pdb:unittests\runtime\SwiftRuntimeTests.pdb /version:0.0 /INCREMENTAL:NO /STACK:10000000 /INCREMENTAL:NO /subsystem:console /MANIFEST /MANIFESTFILE:unittests\runtime\SwiftRuntimeTests.exe.manifest" failed (exit code 1120) with the following output:
   Creating library unittests\runtime\SwiftRuntimeTests.lib and object unittests\runtime\SwiftRuntimeTests.exp
TaskFuture.cpp.obj : error LNK2019: unresolved external symbol "struct swift::FullMetadata<struct swift::TargetOpaqueMetadata<struct swift::InProcess> > const $sSiN" (?$sSiN@@3U?$FullMetadata@U?$TargetOpaqueMetadata@UInProcess@swift@@@swift@@@swift@@B) referenced in function "private: virtual void __cdecl TaskFutureTest_intFuture_Test::TestBody(void)" (?TestBody@TaskFutureTest_intFuture_Test@@EEAAXXZ)
unittests\runtime\SwiftRuntimeTests.exe : fatal error LNK1120: 1 unresolved externals

@DougGregor
Copy link
Member Author

@swift-ci please smoke test Linux

Extend AsyncTask and the concurrency runtime with basic support for
task futures. AsyncTasks with futures contain a future fragment with
information about the type produced by the future, and where the
future will put the result value or the thrown error in the initial
context.

We still don't have the ability to schedule the waiting tasks on an
executor when the future completes, so this isn't useful for anything
just test, and we can only test limited code paths.
Use a single atomic for the wait queue that combines the status with
the first task in the queue. Address race conditions in waiting and
completing the future.

Thanks to John for setting the direction here for me.
Reduce the size of AsyncTask by using the first slot of SchedulerPrivate
for the next waiting task. Thanks, John!
There isn't a big advantage to keeping "simple" task as a separate
concept from a task. Tasks may or may not have futures.
Introduce `FutureAsyncContext` to line up with the async context formed
by IR generation for the type `<T> () async throws -> T`. When allocating
a future task, set up the context with the address of the future's storage
for the successful result and null out the error result, so the caller
will directly fill in the result. This eliminates a bunch of extra
complexity and a copy.
…mber.

'const' breaks compilation on newer compilers.
@DougGregor DougGregor force-pushed the concurrency-task-future branch from 5d38e0c to ede5aa3 Compare November 14, 2020 23:21
@DougGregor
Copy link
Member Author

@swift-ci please test

@DougGregor
Copy link
Member Author

Last commit should clear up the Windows failure. Rerunning all of the tests...

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - ede5aa3

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - ede5aa3

@DougGregor
Copy link
Member Author

@swift-ci pleas test

@DougGregor
Copy link
Member Author

Got tripped up by #34748, but otherwise everything was fine.

@DougGregor
Copy link
Member Author

@swift-ci please smoke test

@DougGregor DougGregor merged commit 5dd0bce into swiftlang:main Nov 15, 2020
@DougGregor DougGregor deleted the concurrency-task-future branch November 15, 2020 04:50
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.

4 participants