-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Isolated synchronous deinit #60057
base: main
Are you sure you want to change the base?
Isolated synchronous deinit #60057
Conversation
14fe1cd
to
0417ee9
Compare
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 fantastic! A couple of comments and questions, both in design and implementation, but it's very much going in the right direction.
| @@ -606,7 +606,8 @@ struct PrintOptions { | |||
| result.SkipSwiftPrivateClangDecls = true; | |||
| result.SkipPrivateStdlibDecls = true; | |||
| result.SkipUnderscoredStdlibProtocols = true; | |||
| result.SkipDeinit = true; | |||
| result.SkipDeinit = false; // Deinit may have isolation attributes, which | |||
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 will make interfaces a little more noisy in the common case, but that's okay. The alternative would be to print only when there is something interesting (i.e., a nonisolated or custom attribute).
stdlib/public/Concurrency/Task.cpp
Outdated
| AdHocJob(JobPriority priority, void *context, AdHocWorkFunction *work) | ||
| : Job({JobKind::AdHoc, priority}, &process), Context(context), | ||
| Work(work) { | ||
| TaskLocal::copyTo(&Local, nullptr); |
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.
There's an interesting design question in here. Do we want the task locals from the task that did the last "release" to be available to the deinitializer? It does match today's semantics, but it could be expensive to copy all of the task locals, which are not likely to be used by many/most deinitializers.
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.
For GAIT, I expect that vast majority of releases to happen within isolating actor, so I'm not very concerned about performance costs of the slow path. But for actors, that's probably not true. Maybe what you mentioned below can help to reduce switching for actors.
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.
I would not depend on having task locals available in a deinit, and especially since we don't guarantee WHICH exact locals those will be, so basing any tracing infra on "object got deallocated and must have right locals" is going to be wrong at some point so not usable "for real" I think...
I'd say losing the locals is expected. For anything that needs things at "end of scope" (like tracing) the usual pattern is to wrap in a closure, not really rely on deinit.
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.
I think some task-locals can still be useful in the deinit even if you are not sure which task/thread performs the last release. For example, if you have a logger applied to entire app or a large subsystem, you can expect that it will be available in any task/thread launched within that scope. So it does not matter which one makes the last release, as long you are confident that last release happens within this coarse-grained scope, you can still meaningfully use logger's task local in the deinit.
|
I would really appreciate a document about how to work with |
|
AFAICS the distributed adjustment looks good 👍 |
8e3201c
to
facc0ca
Compare
a4486cc
to
292f3e1
Compare
| } | ||
| let r = g.wait(timeout: .now() + .milliseconds(500)) | ||
| expectEqual(r, .timedOut) | ||
| expectCrashLater(withMessage: "Assertion failed: (!oldState.getFirstJob() && \"actor has queued jobs at destruction\"), function destroy") |
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.
I'm not sure if that is even valid to test for assertions in stdlib as an expected behaviour. If stdlib is build without assertions, test will fail
|
@swift-ci please test |
1 similar comment
|
@swift-ci please test |
5877715
to
47c4b24
Compare
|
@swift-ci please test |
47c4b24
to
4793499
Compare
|
@swift-ci please test |
|
I asked on the review thread, but maybe it'll get more visibility here. Any chance we can get a toolchain to test? |
|
@swift-ci please build toolchain |
1 similar comment
|
@swift-ci please build toolchain |
324c20d
to
dedd1e4
Compare
|
@swift-ci please build toolchain macOS |
|
@swift-ci please build toolchain |
|
@swift-ci please build toolchain macOS |
…m overriden declaration. But if isolation was inferred from class isolation and matches with isolation of the overriden declaration, attributes are added.
It cannot be used for executing general-purpose work, because such function would need to have a different signature to pass isolated actor instance. And being explicit about using this method only for deinit allows to use object pointer for comparison with executor identity.
* Recursive deallocation with multiple locks held * (UB) Escaping self triggers assertion in stdlib
Failing: Distributed/Runtime/distributed_actor_deinit.swift
…compiler versions
…deinitOnExecutor()
…-deinit # Conflicts: # include/swift/Basic/Features.def # lib/SILGen/SILGenDestructor.cpp # test/Concurrency/flow_isolation.swift # test/abi/macOS/arm64/concurrency.swift # test/abi/macOS/x86_64/concurrency.swift
620fb6d
to
816d62c
Compare
|
Rebased and updated implementation:
Branch is ready to run CI and resume code review process. |
|
@swift-ci please build toolchain |
|
@swift-ci please test |
|
@swift-ci please build toolchain |
|
@swift-ci please test |
|
@swift-ci please build toolchain |
include/swift/ABI/MetadataValues.h
Outdated
| Task_EnqueueJob = 12, | ||
| Task_AddPendingGroupTaskUnconditionally = 13, | ||
| Task_IsDiscardingTask = 14, | ||
| Task_FunctionConsumesContext = 15, |
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.
formatting nitpick
| @@ -326,7 +326,7 @@ SIMPLE_DECL_ATTR(_noAllocation, NoAllocation, | |||
| OnAbstractFunction | OnSubscript | UserInaccessible | ABIStableToAdd | ABIStableToRemove | APIStableToAdd | APIStableToRemove, | |||
| 124) | |||
| SIMPLE_DECL_ATTR(preconcurrency, Preconcurrency, | |||
| OnFunc | OnConstructor | OnProtocol | OnGenericType | OnVar | OnSubscript | OnEnumElement | OnImport | ABIStableToAdd | ABIBreakingToRemove | APIBreakingToAdd | APIBreakingToRemove, | |||
| OnFunc | OnConstructor | OnDestructor | OnProtocol | OnGenericType | OnVar | OnSubscript | OnEnumElement | OnImport | ABIStableToAdd | ABIBreakingToRemove | APIBreakingToAdd | APIBreakingToRemove, | |||
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.
do we need preconcurrency on deinit? I think we talked about how it shouldn't really matter there.
|
|
||
|
|
||
| ERROR(isolated_deinit_no_isolation,none, | ||
| "deinit is marked isolated, but containing class %0 is not isolated to an actor", |
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 needs to handle struct as well because noncopyable struct can have deinits.
ERROR(isolated_deinit_no_isolation,none,
"deinit is marked isolated, but containing %0 %1 is not isolated to an actor",
(DescriptiveDeclKind, DeclName))
here's a patch: 03196c5
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.
So that's the good news; the bad news is that making
@SecondActor
struct SecondActorIsolatedNoncopyableStruct: ~Copyable {
isolated deinit {
#if !SILGEN
isolatedFunc() // expected-error {{call to global actor 'FirstActor'-isolated global function 'isolatedFunc()' in a synchronous global actor 'SecondActor'-isolated context}}
#endif
}
}not blow up seems will take more work; First the emitDistributedRemoteActorDeinit cannot assume we're a class, this is still pretty simple:
// auto cd = cast<ClassDecl>(dd->getDeclContext()->getSelfNominalTypeDecl());
// ->
auto Nominal = dd->getDeclContext()->getSelfNominalTypeDecl();
and adjust accordingly; there's only one place which needs to check for the class there.
Sadly other code will blow up then, this can be looked into afterwards after we land the initial work perhaps as I don't think this is common, but worth taking note.
| case 'Z': | ||
| Args = None; | ||
| Kind = Node::Kind::IsolatedDeallocator; | ||
| break; |
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.
style: nitpick to follow the same style as rest of the switch
| SILValue selfValue, DestructorDecl *dd, bool isIsolated, | ||
| llvm::function_ref<void()> emitLocalDeinit) { | ||
| RegularLocation loc(dd); | ||
| loc.markAutoGenerated(); | ||
|
|
||
| auto cd = cast<ClassDecl>(dd->getDeclContext()->getSelfNominalTypeDecl()); |
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 may fail because ~Copyable sturct's deinit
| @@ -402,20 +401,10 @@ extern "C" SWIFT_CC(swift) void _swift_task_enqueueOnExecutor( | |||
| const SerialExecutorWitnessTable *wtable); | |||
|
|
|||
| SWIFT_CC(swift) | |||
| static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor) { | |||
| static bool isCurrentExecutor(SerialExecutorRef expectedExecutor, | |||
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.
Oh I see, because we need to check for the optimization... this refactoring overlaps (does the same) as some other ongoing work so we'll conflict a bit but that's fine -- yeah you're doing the right thing to try to use the legacy mode during checking here... I think this will be good enough as it's "just" an optimization
| // | ||
| // We always use "legacy" checking mode here, because that's the desired | ||
| // behaviour for this use case. This does not change with SDK version or | ||
| // language mode. |
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.
Ok, sounds good. Indeed as a best effort the legacy mode will give us sometimes a good answer...
I'd love to get the bool returning APIs "always" but it's been a struggle to get those exposed as there's fears about such API being abused.
| } | ||
|
|
||
| func isCurrent(_ a: AnyActor) -> Bool { | ||
| return isCurrentExecutor(getExecutor(a)) |
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.
can't rely on the func returning bool because its runtime dependent, you have to either use an env var to make the test forced into legacy mode -- annoying, or make the test expressed in terms of "check" and just check that we don't crash in situations.
I worry about this just coming back to bite us very soon and us having to refactor all those tests that assume the bool returning 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.
The test can do the same thing really, but let's not assume we're able to return the bool.
In practice returning a false here would become an assertion error so that's still doing what the test should be doing... but sadly these modes make it more confusing than it should be heh.
// I wish we didnt have this behavior but we do so let's try to not double down more on the -> bool "lie"
| } | ||
| let r = g.wait(timeout: .now() + .milliseconds(500)) | ||
| expectEqual(r, .timedOut) | ||
| expectCrashLater(withMessage: "Assertion failed: (!oldState.getFirstUnprioritisedJob() && \"actor has queued jobs at destruction\"), function destroy") |
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 expect... probably should move to the tests.test() {} block
| // Ideally these tests should be compile-time errors | ||
| let tests = TestSuite("EscapingSelf") | ||
| tests.test("escape while locked") { | ||
| // TODO: Investigate failure on Linux and re-enable this test |
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.
I think we can handle this via
// REQUIRES: swift_stdlib_asserts
because we do have some builds which have assertions enabled, so then this test would work, also on linux.
Implementation of the Isolated asynchronous deinit
Still pending:I suspect that logic for adding attributes for inferred isolation is not correct, would like some input from the Core team about this.Still need to integrate with distributed actors, waiting for [Distributed] prevent remote distributed actor's from running deinit bodies #60050 to be merged.Didn't copy changes toBackDeployConcurrencyyet. I expect that I'll have to make changes based on review comments, and want to apply all changes toBackDeployConcurrencyin one go. I would appreciate some advice for maintaining the two in sync.Investigate fast path for self-isolated default actor's deinit