Swift: 为异步/等待函数设置超时

3

有没有一种方法可以附加一个超时时间来停止异步函数,如果它执行时间太长?

这是我代码的简化版本:

func search() async -> ResultEnum {
  // Call multiple Apis one after the other
}

Task.init {
  let searchResult = await search() 
  switch searchResult {
    // Handle all result scenarios
  }
}

我希望search() async函数能够设置截止日期,如果在截止日期之前没有返回结果,它将终止并返回ResultEnum.timeout。请保留HTML标签。

你可能会发现这个链接有用:https://forums.swift.org/t/running-an-async-task-with-a-timeout/49733 确保使用“deadline”而不是“timeout”。 - Alexander
1
在你担心超时之前,你必须先确保 search 是可取消的(而它没有抛出错误的事实表明它是可取消的)。它是可取消的吗? - Rob
嗨@Rob,我可以使其可取消。您能否提供有关如何将我的请求实现为可取消的异步函数的答案? - Wassim
嗨@flatasearth,感谢您的回复。我不介意将截止日期推迟到函数执行。我该如何检查函数内部的执行时间是否超过了时间限制,并停止它? - Wassim
1
一旦您使search可取消(即在取消时停止并可能抛出CancellationError),那么将其包装在某些超时逻辑中就不太困难了。您只需为search创建一个任务,创建另一个任务,在一定时间后将cancel搜索任务,无论哪个任务先完成都会取消另一个任务。请参见https://dev59.com/rsXsa4cB1Zd3GeqPlpLx#74712760。 - Rob
显示剩余2条评论
2个回答

11
谢谢Rob的评论和提供的链接。
不过,我不得不做一些改变。由于某种原因,即使在取消任务后,初始任务fetchTask仍然继续执行,直到我添加了Task.checkCancellation()。
如果有人遇到类似问题,以下是代码的样子:
func search() async throws -> ResultEnum {
  // This is the existing method as per my initial question.
  // It calls multiple Apis one after the other, then returns a result.
}

// Added the below method to introduce a deadline for search()
func search(withTimeoutSecs: Int) async {
    let fetchTask = Task {
        let taskResult = try await search()
        try Task.checkCancellation()
        // without the above line, search() kept going until server responded long after deadline.
        return taskResult
    }
        
    let timeoutTask = Task {
        try await Task.sleep(nanoseconds: UInt64(withTimeoutSecs) * NSEC_PER_SEC)
        fetchTask.cancel()
    }
        
    do {
        let result = try await fetchTask.value
        timeoutTask.cancel()
        return result
    } catch {
        return ResultEnum.failed(NetworkError.timeout)
    }
}

// Call site: Using the function (withTimeout:) instead of ()
Task.init {
    let searchResult = await search(withTimeoutSecs: 6) 
    switch searchResult {
      // Handle all result scenarios
    }
}

1
дёҖдёӘеҸҜеҸ–ж¶Ҳзҡ„гҖҒжҠӣеҮәејӮеёёзҡ„asyncеҮҪж•°йҖҡеёёеә”иҜҘеңЁиў«еҸ–ж¶Ҳж—¶иҮӘе·ұжҠӣеҮәCancellationErrorгҖӮеҜ№дәҺtry Task.checkCancellation()зҡ„иҙҹжӢ…йҖҡеёёдёҚеә”иҜҘиҗҪеңЁи°ғз”ЁиҖ…зҡ„иӮ©дёҠгҖӮеңЁжҲ‘зңӢжқҘпјҢй—®йўҳеңЁдәҺдҪ зҡ„searchеҮҪж•°гҖӮдҪҶжҳҜпјҢжҳҫ然пјҢеҰӮжһңsearchжІЎжңүд»ҘжӯЈзЎ®зҡ„ж–№ејҸеӨ„зҗҶеҸ–ж¶Ҳж“ҚдҪңпјҢйӮЈд№ҲжҲ‘зҢңдҪ еҸӘиғҪеҒҡдҪ еҝ…йЎ»еҒҡзҡ„дәӢжғ…дәҶгҖӮ - Rob
1
我们需要看到“搜索”实现才能进一步评论。 - Rob

0
取决于您需要的超时类型,但我制定了其中一种超时方法。这种方法通过超时发生时取消当前任务,并继续向前丢弃等待任务。
取消任务实际上并没有停止当前操作,只是告诉它们已取消。
下面的代码调用取消任务,但“完成”打印将在longOperation完成后才完成,除非该操作具有取消处理。
let task = Task {
  await longOperation()
  print("done")
}

...

task.cancel()

那么,让我们来想象以下情况。
await fetchFlag() // want this to make timeout in 5sec.

applyFlag()

这个案例需要在任何情况下都在5秒内获取标志的超时。 是否提出取消请求实际上停止当前操作并弹出它们的步骤取决于实现。 为了以任何代价丢弃处理它们而使超时。 (所以实际上获取请求仍在进行中,但仍然向前移动,因为已超时。)
await withTimeout(5) {
  await fetchFlag() // want this to make timeout in 5sec.
}

applyFlag() // apply new flag or current flag if timeout occurred.

为了实现这种行为,需要在内部利用非结构化并发API。
public enum TimeoutHandlerError: Error {
  case timeoutOccured
}

@_unsafeInheritExecutor
public func withTimeout<Return: Sendable>(
  nanoseconds: UInt64,
  @_inheritActorContext _ operation: @escaping @Sendable () async throws -> Return
) async throws -> Return {

  let task = Ref<Task<(), Never>>(value: nil)
  let timeoutTask = Ref<Task<(), any Error>>(value: nil)

  let flag = Flag()

  return try await withTaskCancellationHandler {

    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Return, Error>) in

      do {
        try Task.checkCancellation()
      } catch {
        continuation.resume(throwing: error)
        return
      }

      let _task = Task {
        do {
          let taskResult = try await operation()

          await flag.performIf(expected: false) {
            continuation.resume(returning: taskResult)
            return true
          }
        } catch {
          await flag.performIf(expected: false) {
            continuation.resume(throwing: error)
            return true
          }
        }
      }

      task.value = _task

      let _timeoutTask = Task {
        try await Task.sleep(nanoseconds: nanoseconds)
        _task.cancel()

        await flag.performIf(expected: false) {
          continuation.resume(throwing: TimeoutHandlerError.timeoutOccured)
          return true
        }

      }

      timeoutTask.value = _timeoutTask
    }
  } onCancel: {
    task.value?.cancel()
    timeoutTask.value?.cancel()
  }
}


private final class Ref<T>: @unchecked Sendable {
  var value: T?

  init(value: T?) {
    self.value = value
  }
}

private actor Flag {
  var value: Bool = false

  func set(value: Bool) {
    self.value = value
  }

  func performIf(expected: Bool, perform: @Sendable () -> Bool) {
    if value == expected {
      value = perform()
    }
  }
}

https://github.com/muukii/swift-async-timeout/blob/main/Sources/swift-async-timeout/swift_async_timeout.swift


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接