swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.68k stars 10.39k forks source link

`Task` created within `withUnsafeContinuation` breaks actor isolation #62503

Open dfed opened 1 year ago

dfed commented 1 year ago

Description A Task created from within a withUnsafeContinuation closure executed within an actor-isolated method executes outside of the actor's isolated context.

This issue is quite similar to #61485, except that:

  1. It occurs on Xcode 14.1 when running against macOS 13.0.1 and tvOS 16.1, but not iOS 16.1.
  2. It occurs within a Task created within a withUnsafeContinuation closure.

Steps to reproduce The following test fails when run on Xcode 14.1 and macOS 13.0.1 and tvOS 16.1:

func testUncheckedContinuationTaskActorIsolation() async {
    actor TestActor {
        func execute(_ block: @escaping @Sendable () -> Void) async {
            await withUnsafeContinuation { continuation in
                Task {
                    // This code should be (but isn't) executing within the actor context.
                    continuation.resume()
                    block()
                }
            }
        }
    }

    final class Counter: @unchecked Sendable {
        func incrementAndExpectCount(equals expectedCount: Int, file: StaticString = #filePath, line: UInt = #line) {
            let incrementedCount: Int
            lock.lock()
            count += 1
            incrementedCount = count
            lock.unlock()
            XCTAssertEqual(expectedCount, incrementedCount, file: file, line: line)
        }
        private var count: Int = 0
        private let lock = NSLock()
    }

    let testActor = TestActor()
    let counter = Counter()
    for iteration in 1...1_000_000 {
        await testActor.execute {
            counter.incrementAndExpectCount(equals: iteration)
        }
    }
}

Expected behavior I expect that the above test succeeds on all platforms.

This test can be made to succeed on all platforms by updating the TestActor as follows:

actor TestActor {
    func execute(_ block: @escaping @Sendable () -> Void) async {
        await withUnsafeContinuation { continuation in
            Task {
+               // Force this task to execute within the actor context by accessing an ivar on the instance.
+               _ = void
+
-               // This code should be (but isn't) executing within the actor context.
+               // This code is executing within the actor context.
                continuation.resume()
                block()
            }
        }
    }
+
+   private let void: Void = ()
}

Environment

Additional context This issue was discovered while trying to create an ordered executor for async tasks that can be enqueued from a nonisolated synchronous context.

I have not tested this code against watchOS just yet.

dfed commented 1 year ago

This issue also occurs on the release candidate Xcode 14.2 (14C18) with the following Swift compiler version:

swift-driver version: 1.62.15 Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0