Skip to content

Commit 9e8f2cc

Browse files
committed
[Concurrency] Task cancellation and deadline stubs
1 parent e29c19c commit 9e8f2cc

File tree

6 files changed

+346
-1
lines changed

6 files changed

+346
-1
lines changed

stdlib/public/Concurrency/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ add_swift_target_library(swift_Concurrency ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} I
1515
PartialAsyncTask.swift
1616
Task.cpp
1717
Task.swift
18+
TaskCancellation.swift
19+
_TimeTypes.swift
1820
TaskAlloc.cpp
1921
TaskStatus.cpp
2022
Mutex.cpp

stdlib/public/Concurrency/Task.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public enum Task {
3939
extension Task {
4040

4141
/// Returns the current task's priority.
42+
///
43+
/// ### Suspension
44+
/// This function returns instantly and will never suspend.
45+
/* @instantaneous */
4246
public static func currentPriority() async -> Priority {
4347
fatalError("\(#function) not implemented yet.")
4448
}
@@ -251,6 +255,10 @@ extension Task {
251255
/// The operation functions must resume the continuation *exactly once*.
252256
///
253257
/// The continuation will not begin executing until the operation function returns.
258+
///
259+
/// ### Suspension
260+
/// This function returns instantly and will never suspend.
261+
/* @instantaneous */
254262
public static func withUnsafeContinuation<T>(
255263
operation: (UnsafeContinuation<T>) -> Void
256264
) async -> T {
@@ -260,6 +268,10 @@ extension Task {
260268
/// The operation functions must resume the continuation *exactly once*.
261269
///
262270
/// The continuation will not begin executing until the operation function returns.
271+
///
272+
/// ### Suspension
273+
/// This function returns instantly and will never suspend.
274+
/* @instantaneous */
263275
public static func withUnsafeThrowingContinuation<T>(
264276
operation: (UnsafeThrowingContinuation<T, Error>) -> Void
265277
) async throws -> T {
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
////===----------------------------------------------------------------------===//
2+
////
3+
//// This source file is part of the Swift.org open source project
4+
////
5+
//// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
//// Licensed under Apache License v2.0 with Runtime Library Exception
7+
////
8+
//// See https://swift.org/LICENSE.txt for license information
9+
//// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
////
11+
////===----------------------------------------------------------------------===//
12+
13+
import Swift
14+
@_implementationOnly import _SwiftConcurrencyShims
15+
16+
// ==== Task Cancellation ------------------------------------------------------
17+
18+
extension Task {
19+
20+
/// Returns `true` if the task is cancelled, and should stop executing.
21+
///
22+
/// ### Suspension
23+
/// This function returns instantly and will never suspend.
24+
///
25+
/// - SeeAlso: `checkCancellation()`
26+
/* @instantaneous */
27+
public static func isCancelled() async -> Bool {
28+
// let task = __getTask() // TODO: pending internal API to get tasks
29+
// task.isCancelled || task.deadline.isOverdue
30+
fatalError("\(#function) not implemented yet.")
31+
}
32+
33+
/// Check if the task is cancelled and throw an `CancellationError` if it was.
34+
///
35+
/// It is intentional that no information is passed to the task about why it
36+
/// was cancelled. A task may be cancelled for many reasons, and additional
37+
/// reasons may accrue / after the initial cancellation (for example, if the
38+
/// task fails to immediately exit, it may pass a deadline).
39+
///
40+
/// The goal of cancellation is to allow tasks to be cancelled in a
41+
/// lightweight way, not to be a secondary method of inter-task communication.
42+
///
43+
/// ### Suspension
44+
/// This function returns instantly and will never suspend.
45+
///
46+
/// - SeeAlso: `isCancelled()`
47+
/* @instantaneous */
48+
public static func checkCancellation() async throws {
49+
if await Task.isCancelled() {
50+
throw CancellationError()
51+
}
52+
}
53+
54+
/// Execute an operation with cancellation handler which will immediately be
55+
/// invoked if the current task is cancelled.
56+
///
57+
/// This differs from the operation cooperatively checking for cancellation
58+
/// and reacting to it in that the cancellation handler is _always_ and
59+
/// _immediately_ invoked when the task is cancelled. For example, even if the
60+
/// operation is running code which never checks for cancellation, a cancellation
61+
/// handler still would run and give us a chance to run some cleanup code.
62+
///
63+
/// Does not check for cancellation, and always executes the passed `operation`.
64+
///
65+
/// ### Suspension
66+
/// This function returns instantly and will never suspend.
67+
/* @instantaneous */
68+
public static func withCancellationHandler<T>(
69+
handler: /* @concurrent */ () -> (),
70+
operation: () async throws -> T
71+
) async throws -> T {
72+
fatalError("\(#function) not implemented yet.")
73+
}
74+
75+
/// The default cancellation thrown when a task is cancelled.
76+
///
77+
/// This error is also thrown automatically by `Task.checkCancellation()`,
78+
/// if the current task has been cancelled.
79+
public struct CancellationError: Error {
80+
// no extra information, cancellation is intended to be light-weight
81+
}
82+
83+
}
84+
85+
// ==== Task Deadlines ---------------------------------------------------------
86+
87+
extension Task {
88+
89+
/// Returns the earliest deadline set on the current task.
90+
///
91+
/// If no deadline was set for the task the `Deadline.distantFuture` is returned,
92+
/// as it is effective in conveying that there still is time remaining and the
93+
/// deadline is not overdue yet.
94+
///
95+
/// ### Suspension
96+
/// This function returns instantly and will never suspend.
97+
/* @instantaneous */
98+
public static func currentDeadline() async -> Deadline {
99+
fatalError("\(#function) not implemented yet.")
100+
}
101+
102+
/// Execute a code block with a deadline in `interval`.
103+
///
104+
/// If the current task already has a deadline set that is _prior_
105+
/// to the newly set deadline with this API, that deadline will remain in effect.
106+
///
107+
/// This allows higher level tasks to set an upper bound on the deadline they
108+
/// are willing cumulatively willing to wait for the entire task to execute,
109+
/// regardless of the inner deadlines of the specific child tasks.
110+
///
111+
/// Cancellation is co-operative and must be checked for by the operation, e.g.
112+
/// by invoking `Task.checkCancellation`, or `Task.isCancelled`.
113+
///
114+
/// ### Suspension
115+
/// This function returns instantly and will never suspend.
116+
///
117+
/// - Parameters:
118+
/// - interval: interval after which (from `now()`) the operation task should
119+
/// be considered cancelled.
120+
/// - operation: the operation to execute
121+
/* @instantaneous */
122+
public static func withDeadline<T>(
123+
in interval: _TimeInterval,
124+
operation: () async throws -> T
125+
) async rethrows -> T {
126+
fatalError("\(#function) not implemented yet.")
127+
}
128+
129+
/// Execute a code block with the passed in deadline (unless a shorter deadline is already set).
130+
///
131+
/// If the current task already has a deadline set that is _prior_
132+
/// to the newly set deadline with this API, that deadline will remain in effect.
133+
///
134+
/// This allows higher level tasks to set an upper bound on the deadline they
135+
/// are willing cumulatively willing to wait for the entire task to execute,
136+
/// regardless of the inner deadlines of the specific child tasks.
137+
///
138+
/// Cancellation is co-operative and must be checked for by the operation, e.g.
139+
/// by invoking `Task.checkCancellation` or `Task.isCancelled`.
140+
///
141+
/// ### Suspension
142+
/// This function returns instantly and will never suspend.
143+
///
144+
/// - Parameters:
145+
/// - deadline: the point in time after which the operation task should be
146+
/// considered cancelled.
147+
/// - operation: the operation to execute
148+
/* @instantaneous */
149+
public static func withDeadline<T>(
150+
_ deadline: Deadline,
151+
operation: () async throws -> T
152+
) async rethrows -> T {
153+
fatalError("\(#function) not implemented yet.")
154+
}
155+
156+
/// A deadline is a point in time past-which a task should be considered cancelled.
157+
///
158+
/// Deadlines function the same was as pure cancellation, in the sense that they
159+
/// are cooperative and require the cancelled (deadline exceeding) task to check
160+
/// for this as it is performing its execution.
161+
///
162+
/// Generally tasks (or partial tasks) should perform such check before they
163+
/// start executing, however this is not a strict rule, and some tasks may
164+
/// choose to be un-cancellable.
165+
public struct Deadline {
166+
public typealias WallTime = UInt64 // equivalent to DispatchWallTime
167+
internal let time: WallTime
168+
169+
public init(at time: WallTime) {
170+
self.time = time
171+
}
172+
173+
public static var distantFuture: Self {
174+
.init(time: .max)
175+
}
176+
177+
public static func `in`(_ interval: _TimeInterval) -> Self {
178+
// now() + interval
179+
fatalError("#\(#function) not implemented yet.")
180+
}
181+
182+
/// Returns `true` if the deadline is overdue and deadline should be
183+
/// considered overdue (or "exceeded").
184+
///
185+
/// If this deadline was related to a `Task`, that task should be considered
186+
/// cancelled if the deadline is overdue.
187+
public var isOverdue: Bool {
188+
!self.hasTimeLeft
189+
}
190+
191+
/// Returns `true` if the deadline is still pending with respect to "now".
192+
public var hasTimeLeft: Bool {
193+
fatalError("\(#function) not implemented yet.")// self.hasTimeLeft(until: now())
194+
}
195+
196+
// TODO: public func hasTimeLeft(until: DispatchWallTime or whichever time type we'll use) -> Bool
197+
198+
}
199+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
////===----------------------------------------------------------------------===//
2+
////
3+
//// This source file is part of the Swift.org open source project
4+
////
5+
//// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
//// Licensed under Apache License v2.0 with Runtime Library Exception
7+
////
8+
//// See https://swift.org/LICENSE.txt for license information
9+
//// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
////
11+
////===----------------------------------------------------------------------===//
12+
13+
import Swift
14+
@_implementationOnly import _SwiftConcurrencyShims
15+
16+
// FIXME: This file and all "time types" defined here are temporary until we decide what types to use.
17+
// It was suggested to avoid Dispatch types in public API of Swift Concurrency,
18+
// so for now we use "bare minimum" types just to be able to continue prototyping.
19+
extension Task {
20+
/// Represents a time interval, i.e. a number of seconds.
21+
///
22+
/// It can be used to express deadlines, in the form of time interval from "now."
23+
///
24+
/// - Note: This is equivalent to `DispatchTimeInterval` if we were to use it.
25+
public struct _TimeInterval: Equatable, Comparable {
26+
let nanoseconds: UInt64
27+
28+
private init(nanoseconds: UInt64) {
29+
self.nanoseconds = nanoseconds
30+
}
31+
32+
public static func seconds(_ s: UInt64) -> Self {
33+
.init(nanoseconds: clampedInt64Product(s, 1_000_000_000))
34+
}
35+
36+
public static func milliseconds(_ ms: UInt64) -> Self {
37+
.init(nanoseconds: clampedInt64Product(ms, 1_000_000))
38+
}
39+
40+
public static func microseconds(_ us: UInt64) -> Self {
41+
.init(nanoseconds: clampedInt64Product(us, 1000))
42+
}
43+
44+
public static func nanoseconds(_ ns: UInt64) -> Self {
45+
.init(nanoseconds: ns)
46+
}
47+
48+
public static var never: Self {
49+
.init(nanoseconds: .max)
50+
}
51+
52+
public static func < (lhs: Self, rhs: Self) -> Bool {
53+
lhs.nanoseconds < rhs.nanoseconds
54+
}
55+
}
56+
57+
}
58+
59+
// Returns m1 * m2, clamped to the range [UInt64.min, UInt64.max].
60+
private func clampedInt64Product(_ m1: UInt64, _ m2: UInt64) -> UInt64 {
61+
let (result, overflow) = m1.multipliedReportingOverflow(by: m2)
62+
if overflow {
63+
return UInt64.max
64+
}
65+
return result
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency
2+
// REQUIRES: concurrency
3+
4+
enum PictureData {
5+
case value(String)
6+
case failedToLoadImagePlaceholder
7+
}
8+
9+
func test_cancellation_checkCancellation() async throws {
10+
await try Task.checkCancellation()
11+
}
12+
13+
func test_cancellation_guard_isCancelled(_ any: Any) async -> PictureData {
14+
guard await !Task.isCancelled() else {
15+
return PictureData.failedToLoadImagePlaceholder
16+
}
17+
18+
return PictureData.value("...")
19+
}
20+
21+
struct SomeFile {
22+
func close() {}
23+
}
24+
25+
func test_cancellation_withCancellationHandler(_ anything: Any) async -> PictureData {
26+
let handle = Task.runDetached { () -> PictureData in
27+
let file = SomeFile()
28+
29+
return await try Task.withCancellationHandler(
30+
handler: { file.close() },
31+
operation: {
32+
await test_cancellation_guard_isCancelled(file)
33+
})
34+
}
35+
36+
handle.cancel()
37+
}
38+
39+
func test_cancellation_loop() async -> Int {
40+
struct SampleTask { func process() async {} }
41+
42+
let tasks = [SampleTask(), SampleTask()]
43+
var processed = 0
44+
for t in tasks where await !Task.isCancelled() {
45+
await t.process()
46+
processed += 1
47+
}
48+
return processed
49+
}
50+
51+
// ==== Deadlines --------------------------------------------------------------
52+
53+
func int() async -> Int { 42 }
54+
55+
func test_cancellation_withDeadline_in() async throws -> Int {
56+
/* missing await */ Task.withDeadline(in: .seconds(5), operation: { // FIXME: rdar://70751405 async rethrows functions are not detected as async
57+
await int()
58+
})
59+
}
60+
61+
func test_cancellation_withDeadline(specificDeadline: Task.Deadline) async -> Int {
62+
/* missing `await` */
63+
Task.withDeadline(specificDeadline) { // FIXME: rdar://70751405 async rethrows functions are not detected as async
64+
await int()
65+
}
66+
}

test/expr/unary/async_await.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,4 @@ func invalidAsyncFunction() async {
143143

144144
func validAsyncFunction() async throws {
145145
_ = try await throwingAndAsync()
146-
}
146+
}

0 commit comments

Comments
 (0)