Skip to content
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

[SR-14802] Concurrency: Unstructured Task Triggering Continuation Can Hang #57150

Open
jshier opened this issue Jun 21, 2021 · 7 comments
Open
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.

Comments

@jshier
Copy link
Contributor

jshier commented Jun 21, 2021

Previous ID SR-14802
Radar rdar://problem/79563720
Original Reporter @jshier
Type Bug

Attachment: Download

Environment

Xcode 13b1, default and 6/14 5.5 toolchain, iOS 15 simulator

Additional Detail from JIRA
Votes 2
Component/s
Labels Bug
Assignee None
Priority Medium

md5: 299292dc9504c5fcebe4846384790ce5

is duplicated by:

  • SR-14875 Resuming continuations from actor contexts can hang

Issue Description:

As discussed on the forums: https://forums.swift.org/t/concurrency-hang-when-continuations-resumed-in-task-but-not-in-actor/49772/6

I create a continuation to allow users to wait for network data captured by an actor and capture its resume in a stored closure. When that capture is complete, I call the enqueued closures to trigger the continuations. Strictly calling the closure within an actor method works fine. Wrapping the closure call in an unstructured task causes a hang and the waiter is never notified. However, simply adding a print statement at the end of the actor method allows it to work. My test iOS project is attached.

@typesanitizer
Copy link

@swift-ci create

@jshier
Copy link
Contributor Author

jshier commented Jun 26, 2021

This seems to have regressed further in beta 2. Now none of my awaited work seems to complete unless I put a `print` statement in the `request` method before returning the `DataRequest` value. The `print` at the end of `didComplete` is still necessary if the continuations are resumed within an unstructured task.

@jshier
Copy link
Contributor Author

jshier commented Jul 16, 2021

This is still an issue in beta 3. A print statement in the request method is required for any of the work to start.

@josephlord
Copy link

For information. I'm still seeing failures with this test case in Xcode beta 5 (running on iPhone Simulator):

import XCTest


@available(iOS 15.0, macOS 12.0, *)
actor SUTActor {
    var continuation: CheckedContinuation<(),Never>?
    var canGo = false
    
    func pause() async {
        print("in pause")
        if canGo {
            print("canGo true before awat")
            return
        }
        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) -> Void in
            print("in withCheckedContinuation operation")
            if canGo {
                print("canGo true after awit")
                Task.detached { continuation.resume() }
            } else {
                self.continuation = continuation
            }
            print("completing withCheckedOperation closure")
        }
        print("awaitedContinuation resumed")
    }
    
    func go() {
        print("in go")
        canGo = true
        let continuation = self.continuation
        print(continuation == nil ? "nil cont" : "has cont")
        self.continuation = nil
        Task.detached {
            print("resuming continuation")
            continuation?.resume()
            print("did resume continuation")
        }
    }
}


@available(iOS 15.0, macOS 12.0, *)
final class ActorContinuationTests : XCTestCase {
    func testPauseGo() {
        let sut = SUTActor()
        let exp = expectation(description: "Will resume")
        Task.detached(priority: .high) {
            print("will await pause")
            await sut.pause()
            print("pause returned")
            exp.fulfill()
        }
        Task.detached(priority: .medium) {
            print("will await go")
            await sut.go()
            print("Boom")
        }
        waitForExpectations(timeout: 0.3, handler: nil)
        _ = sut // Ensure lifetime sufficient
    }
}

A failed run prints:

will await pause
will await go
in pause
in withCheckedContinuation operation
completing withCheckedOperation closure

So it seems that it never actually enters the `go()` function. When it works it seems that the withCheckedContinuationClosure is completed before go is awaited.

This may actually be more related to the issue in release notes "Swift tasks won't have their priority escalated in response to awaiting on their handles. (76127623)" although I don't think there needs to be any escalation. The issue does seem dependent on the different priorities and other test cases seem to now be working. As before with my test cases this doesn't fail every time but does fail a few times in every hundred.

@typesanitizer
Copy link

Hmm, this should've been fixed in beta 5. The change that was made was that all priorities get "squashed" to the unspecified level (because the smarter scheduling that took priorities into account wasn't working) so high vs low priority shouldn't matter.

I'll double-check on whether the change landed in time for beta 5 or not.

@typesanitizer
Copy link

Looks like not all the patches landed in beta 5, in particular this one: #38709

@josephlord In the meantime, if you do not specify priorities, does the issue go away or are you still seeing the issue without explicit priorities?

@josephlord
Copy link

Yes. I can’t reproduce without the priorities.

It isn’t blocking anything for me. Just wanted to report results of re-running my exploratory test cases.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior.
Projects
None yet
Development

No branches or pull requests

3 participants