Uploaded image for project: 'Swift'
  1. Swift
  2. SR-14875

Resuming continuations from actor contexts can hang

    XMLWordPrintable

    Details

    • Type: Bug
    • Status: Resolved
    • Priority: Medium
    • Resolution: Duplicate
    • Component/s: None
    • Labels:
      None
    • Environment:

      Xcode 13 beta 2. Snapshot builds 5.5 toolchain 2021-07-03, 2021-07-01, 2021-06-29, M1 Mac mini, Monterey beta2, Xcode beta 3, Monterey beta 3

      Description

      Note also separate hang report here: SR-14802

      I've been playing with and testing creating my own AsyncSequences. The first implementation below works well, the second is highly unreliable. The only difference is that  the first one returns the continuation from the actor which then gets resumed and the second one it is resumed from the actor context.

      I'm not aware of anything in the specification about not firing continuations from actor context. If it really shouldn't be done there should be compile time errors if there is an attempt to resume a continuation from an actor context.

      @available(macOS 12.0, iOS 15.0, *)
      public struct AsyncTimerActorSequence : AsyncSequence {
          
          public typealias AsyncIterator = Iterator
          public typealias Element = Void
          
          let interval: TimeInterval
          
          public init(interval: TimeInterval) {
              self.interval = interval
          }
          
          public func makeAsyncIterator() -> Iterator {
              Iterator(interval: interval)
          }
          
          public class Iterator : AsyncIteratorProtocol {
              
              private actor InnerActor {
                  private var continuations: Deque<CheckedContinuation<(), Never>> = []
                  
                  fileprivate func getContinuation() -> CheckedContinuation<(), Never>? {
                      return continuations.popFirst()
                  }
                  
                  fileprivate func addContinuation(_ continuation: CheckedContinuation<(), Never>) {
                      continuations.append(continuation)
                  }
              }
              private let safeContinuations = InnerActor()
              private var timer: Timer?
              
              fileprivate init(interval: TimeInterval) {
                  let safeConts = safeContinuations
                  let t = Timer(fire: .now, interval: interval, repeats: true) { _ in
                      Task {
                          let continuation = await safeConts.getContinuation()
                          continuation?.resume()
                      }
                  }
                  self.timer = t
                  RunLoop.main.add(t, forMode: .default)
              }
              
              public func next() async throws -> ()? {
                  await withCheckedContinuation { continuation in
                      Task {
                          await safeContinuations.addContinuation(continuation)
                      }
                  }
                  return ()
              }
              
              deinit {
                  timer?.invalidate()
              }
          }
      }
      
      
      @available(macOS 12.0, iOS 15.0, *)
      public struct AsyncTimerActorSequenceUnreliable : AsyncSequence {
          
          public typealias AsyncIterator = Iterator
          public typealias Element = Void
          
          let interval: TimeInterval
          
          public init(interval: TimeInterval) {
              self.interval = interval
          }
          
          public func makeAsyncIterator() -> Iterator {
              Iterator(interval: interval)
          }
          
          public class Iterator : AsyncIteratorProtocol {
              
              private actor InnerActor {
                  private var continuations: Deque<CheckedContinuation<(), Never>> = []
                  
                  fileprivate func fireContinuation() {
                      continuations.popFirst()?.resume()
                  }
                  
                  fileprivate func addContinuation(_ continuation: CheckedContinuation<(), Never>) {
                      continuations.append(continuation)
                  }
              }
              private let safeContinuations = InnerActor()
              private var timer: Timer?
              
              fileprivate init(interval: TimeInterval) {
                  let safeConts = safeContinuations
                  let t = Timer(fire: .now, interval: interval, repeats: true) { _ in
                      Task {
                          await safeConts.fireContinuation()
                      }
                  }
                  self.timer = t
                  RunLoop.main.add(t, forMode: .default)
              }
              
              public func next() async throws -> ()? {
                  await withCheckedContinuation { continuation in
                      Task {
                          await safeContinuations.addContinuation(continuation)
                      }
                  }
                  return ()
              }
              
              deinit {
                  timer?.invalidate()
              }
          }
      }
      

      To reproduce open the package in Xcode and run the AsyncTimerSequenceUnreliableActorTests repeatedly (100 times). There should be some errors. Whereas the similar AsyncTimerSequenceActorTests should be the same tests but on the working version of the sequence.

        Attachments

          Issue Links

            Activity

              People

              Assignee:
              Unassigned Unassigned
              Reporter:
              josephlord Joseph Lord
              Votes:
              1 Vote for this issue
              Watchers:
              3 Start watching this issue

                Dates

                Created:
                Updated:
                Resolved: