-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Concurrency] Add API stubs for Task #34391
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9023e56
[Concurrency] Add minimal placeholders for Task and UnsafeContinuation
ktoso 07f80be
[Concurrency] API stubs: Task.Handle, priority and runDetached
ktoso d6adac3
[Concurrency] Stubs for Task.current()
ktoso 579c89c
[Concurrency] More documentation of Task.Priority
ktoso b5fd2a5
Address review comments; get() must throw, formatting
ktoso File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
////===----------------------------------------------------------------------===// | ||
//// | ||
//// This source file is part of the Swift.org open source project | ||
//// | ||
//// Copyright (c) 2020 Apple Inc. and the Swift project authors | ||
//// Licensed under Apache License v2.0 with Runtime Library Exception | ||
//// | ||
//// See https://swift.org/LICENSE.txt for license information | ||
//// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors | ||
//// | ||
////===----------------------------------------------------------------------===// | ||
|
||
import Swift | ||
@_implementationOnly import _SwiftConcurrencyShims | ||
|
||
// ==== Task ------------------------------------------------------------------- | ||
|
||
/// An asynchronous task (just "Task" hereafter) is the analogue of a thread for | ||
/// asynchronous functions. All asynchronous functions run as part of some task. | ||
/// | ||
/// A task's execution can be seen as a series of periods where the task was | ||
/// running. Each such period ends at a suspension point or -- finally -- the | ||
/// completion of the task. | ||
/// | ||
/// These partial periods towards the task's completion are `PartialAsyncTask`. | ||
/// Partial tasks are generally not interacted with by end-users directly, | ||
/// unless implementing a scheduler. | ||
public struct Task { | ||
} | ||
|
||
// ==== Current Task ----------------------------------------------------------- | ||
|
||
extension Task { | ||
/// Returns the currently executing `Task`. | ||
/// | ||
/// As invoking this function is only possible from an asynchronous context | ||
/// it is always able to return the current `Task` in which we are currently | ||
/// running. | ||
public static func current() async -> Task { | ||
fatalError("\(#function) not implemented yet.") // TODO: needs a built-in function | ||
} | ||
} | ||
|
||
// ==== Task Priority ---------------------------------------------------------- | ||
|
||
extension Task { | ||
/// Task priority may inform decisions an `Executor` makes about how and when | ||
/// to schedule tasks submitted to it. | ||
/// | ||
/// ### Priority scheduling | ||
/// An executor MAY utilize priority information to attempt running higher | ||
/// priority tasks first, and then continuing to serve lower priority tasks. | ||
/// | ||
/// The exact semantics of how priority is treated are left up to each | ||
/// platform and `Executor` implementation. | ||
/// | ||
/// ### Priority inheritance | ||
/// Child tasks automatically inherit their parent task's priority. | ||
/// | ||
/// Detached tasks (created by `Task.runDetached`) DO NOT inherit task priority, | ||
/// as they are "detached" from their parent tasks after all. | ||
/// | ||
/// ### Priority elevation | ||
/// In some situations the priority of a task must be elevated ("raised"): | ||
/// | ||
/// - if a `Task` running on behalf of an actor, and a new higher-priority | ||
/// task is enqueued to the actor, its current task must be temporarily | ||
/// elevated to the priority of the enqueued task, in order to allow the new | ||
/// task to be processed at--effectively-- the priority it was enqueued with. | ||
/// - this DOES NOT affect `Task.currentPriority()`. | ||
/// - if a task is created with a `Task.Handle`, and a higher-priority task | ||
/// calls the `await handle.get()` function the priority of this task must be | ||
/// permanently increased until the task completes. | ||
/// - this DOES affect `Task.currentPriority()`. | ||
/// | ||
/// TODO: Define the details of task priority; It is likely to be a concept | ||
/// similar to Darwin Dispatch's QoS; bearing in mind that priority is not as | ||
/// much of a thing on other platforms (i.e. server side Linux systems). | ||
public struct Priority: Comparable { | ||
public static let `default`: Task.Priority = .init() // TODO: replace with actual values | ||
|
||
// TODO: specifics of implementation are not decided yet | ||
private let __value: Int = 0 | ||
|
||
public static func < (lhs: Self, rhs: Self) -> Bool { | ||
lhs.__value < rhs.__value | ||
} | ||
} | ||
} | ||
|
||
// ==== Task Handle ------------------------------------------------------------ | ||
|
||
extension Task { | ||
|
||
/// A task handle refers to an in-flight `Task`, | ||
/// allowing for potentially awaiting for its result or canceling it. | ||
/// | ||
/// It is not a programming error to drop a handle without awaiting or canceling it, | ||
/// i.e. the task will run regardless of the handle still being present or not. | ||
/// Dropping a handle however means losing the ability to await on the task's result | ||
/// and losing the ability to cancel it. | ||
public final class Handle<Success, Failure: Error> { | ||
/// Wait for the task to complete, returning (or throwing) its result. | ||
/// | ||
/// ### Priority | ||
/// If the task has not completed yet, its priority will be elevated to the | ||
/// priority of the current task. Note that this may not be as effective as | ||
/// creating the task with the "right" priority to in the first place. | ||
/// | ||
/// ### Cancellation | ||
/// If the awaited on task gets cancelled the `get()` will throw a cancellation error. | ||
public func get() async throws -> Success { | ||
fatalError("\(#function) not implemented yet.") | ||
ktoso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// Attempt to cancel the task. | ||
/// | ||
/// Whether this function has any effect is task-dependent. | ||
/// | ||
/// For a task to respect cancellation it must cooperatively check for it | ||
/// while running. Many tasks will check for cancellation before beginning | ||
/// their "actual work", however this is not a requirement nor is it guaranteed | ||
/// how and when tasks check for cancellation in general. | ||
public func cancel() { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
} | ||
} | ||
|
||
// ==== Detached Tasks --------------------------------------------------------- | ||
|
||
extension Task { | ||
/// Run given `operation` as part of a new top-level task. | ||
/// | ||
/// Creating detached tasks should, generally, be avoided in favor of using | ||
/// `async` functions, `async let` declarations and `await` expressions - as | ||
/// those benefit from structured, bounded concurrency which is easier to reason | ||
/// about, as well as automatically inheriting the parent tasks priority, | ||
/// task-local storage, deadlines, as well as being cancelled automatically | ||
/// when their parent task is cancelled. Detached tasks do not get any of those | ||
/// benefits, and thus should only be used when an operation is impossible to | ||
/// be modelled with child tasks. | ||
/// | ||
/// ### Cancellation | ||
/// A detached task always runs to completion unless it is explicitly cancelled. | ||
/// Specifically, dropping a detached tasks `Task.Handle` does _not_ automatically | ||
/// cancel given task. | ||
/// | ||
/// Canceling a task must be performed explicitly via `handle.cancel()`. | ||
/// | ||
/// - Parameters: | ||
/// - priority: priority of the task TODO: reword and define more explicitly once we have priorities well-defined | ||
/// - operation: | ||
/// - Returns: handle to the task, allowing to `await handle.get()` on the | ||
/// tasks result or `cancel` it. | ||
/// | ||
/// - Note: it is generally preferable to use child tasks rather than detached | ||
/// tasks. Child tasks automatically carry priorities, task-local state, | ||
/// deadlines and have other benefits resulting from the structured | ||
/// concurrency concepts that they model. Consider using detached tasks only | ||
/// when strictly necessary and impossible to model operations otherwise. | ||
public static func runDetached<T>( | ||
priority: Priority = .default, | ||
operation: () async -> T | ||
) -> Handle<T, Never> { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
|
||
/// Run given throwing `operation` as part of a new top-level task. | ||
/// | ||
/// Creating detached tasks should, generally, be avoided in favor of using | ||
/// `async` functions, `async let` declarations and `await` expressions - as | ||
/// those benefit from structured, bounded concurrency which is easier to reason | ||
/// about, as well as automatically inheriting the parent tasks priority, | ||
/// task-local storage, deadlines, as well as being cancelled automatically | ||
/// when their parent task is cancelled. Detached tasks do not get any of those | ||
/// benefits, and thus should only be used when an operation is impossible to | ||
/// be modelled with child tasks. | ||
/// | ||
/// ### Cancellation | ||
/// A detached task always runs to completion unless it is explicitly cancelled. | ||
/// Specifically, dropping a detached tasks `Task.Handle` does _not_ automatically | ||
/// cancel given task. | ||
/// | ||
/// Canceling a task must be performed explicitly via `handle.cancel()`. | ||
/// | ||
/// - Parameters: | ||
/// - priority: priority of the task TODO: reword and define more explicitly once we have priorities well-defined | ||
/// - operation: | ||
/// - Returns: handle to the task, allowing to `await handle.get()` on the | ||
/// tasks result or `cancel` it. If the operation fails the handle will | ||
/// throw the error the operation has thrown when awaited on. | ||
/// | ||
/// - Note: it is generally preferable to use child tasks rather than detached | ||
/// tasks. Child tasks automatically carry priorities, task-local state, | ||
/// deadlines and have other benefits resulting from the structured | ||
/// concurrency concepts that they model. Consider using detached tasks only | ||
/// when strictly necessary and impossible to model operations otherwise. | ||
public static func runDetached<T>( | ||
priority: Priority = .default, | ||
operation: () async throws -> T | ||
) -> Handle<T, Error> { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
} | ||
|
||
// ==== UnsafeContinuation ----------------------------------------------------- | ||
|
||
extension Task { | ||
public struct UnsafeContinuation<T> { | ||
/// Return a value into the continuation and make the task schedulable. | ||
/// | ||
/// The task will never run synchronously, even if the task does not | ||
/// need to be resumed on a specific executor. | ||
/// | ||
/// This is appropriate when the caller is something "busy", like an event | ||
/// loop, and doesn't want to be potentially delayed by arbitrary work. | ||
public func resume(returning: T) { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
} | ||
|
||
public struct UnsafeThrowingContinuation<T, E: Error> { | ||
/// Return a value into the continuation and make the task schedulable. | ||
/// | ||
/// The task will never run synchronously, even if the task does not | ||
/// need to be resumed on a specific executor. | ||
/// | ||
/// This is appropriate when the caller is something "busy", like an event | ||
/// loop, and doesn't want to be potentially delayed by arbitrary work. | ||
public func resume(returning: T) { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
|
||
/// Resume the continuation with an error and make the task schedulable. | ||
/// | ||
/// The task will never run synchronously, even if the task does not | ||
/// need to be resumed on a specific executor. | ||
/// | ||
/// This is appropriate when the caller is something "busy", like an event | ||
/// loop, and doesn't want to be potentially delayed by arbitrary work. | ||
public func resume(throwing: E) { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
} | ||
|
||
/// The operation functions must resume the continuation *exactly once*. | ||
/// | ||
/// The continuation will not begin executing until the operation function returns. | ||
public static func withUnsafeContinuation<T>( | ||
operation: (UnsafeContinuation<T>) -> Void | ||
) async -> T { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
|
||
/// The operation functions must resume the continuation *exactly once*. | ||
/// | ||
/// The continuation will not begin executing until the operation function returns. | ||
public static func withUnsafeThrowingContinuation<T>( | ||
operation: (UnsafeThrowingContinuation<T, Error>) -> Void | ||
) async throws -> T { | ||
fatalError("\(#function) not implemented yet.") | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency | ||
// REQUIRES: concurrency | ||
|
||
func someAsyncFunc() async -> String { "" } | ||
|
||
struct MyError: Error {} | ||
func someThrowingAsyncFunc() async throws -> String { throw MyError() } | ||
|
||
func test_unsafeContinuations() async { | ||
// the closure should not allow async operations; | ||
// after all: if you have async code, just call it directly, without the unsafe continuation | ||
let _: String = Task.withUnsafeContinuation { continuation in // expected-error{{cannot convert value of type '(_) async -> ()' to expected argument type '(Task.UnsafeContinuation<String>) -> Void'}} | ||
let s = await someAsyncFunc() // rdar://70610141 for getting a better error message here | ||
continuation.resume(returning: s) | ||
} | ||
|
||
let _: String = await Task.withUnsafeContinuation { continuation in | ||
continuation.resume(returning: "") | ||
} | ||
} | ||
|
||
func test_unsafeThrowingContinuations() async { | ||
let _: String = try await Task.withUnsafeThrowingContinuation { continuation in | ||
continuation.resume(returning: "") | ||
} | ||
|
||
let _: String = try await Task.withUnsafeThrowingContinuation { continuation in | ||
continuation.resume(throwing: MyError()) | ||
} | ||
|
||
// TODO: Potentially could offer some warnings if we know that a continuation was resumed or escaped at all in a closure? | ||
} | ||
|
||
// ==== Detached Tasks --------------------------------------------------------- | ||
|
||
func test_detached() async throws { | ||
let handle = Task.runDetached() { | ||
await someAsyncFunc() // able to call async functions | ||
} | ||
|
||
let result: String = await try handle.get() | ||
_ = result | ||
} | ||
|
||
func test_detached_throwing() async -> String { | ||
let handle: Task.Handle<String, Error> = Task.runDetached() { | ||
await try someThrowingAsyncFunc() // able to call async functions | ||
} | ||
|
||
do { | ||
return await try handle.get() | ||
} catch { | ||
print("caught: \(error)") | ||
} | ||
} | ||
|
||
// ==== Current Task ----------------------------------------------------------- | ||
|
||
func test_current_task() async { | ||
_ = await Task.current() // yay, we know "in" what task we're executing | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably put an
OpaquePointer
or perhaps anAnyObject
in here and make it frozen, becauseTask
is a thin wrapper around a pointer to a task, which is a ref-counted entity.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah true, will address in next PR 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's swift ref-counted, you should use
Builtin.NativeObject
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the proposal this was just a namespace. If we're going to make it a type that's a generic reference to a task, I think that's useful, but we need to think about how this interacts with
Handle
. Maybe it's a class.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proposal indeed at first declared
enum Task
but also has a fewstruct Task
but I guess those were always meant to beextension Task
-- I guess it boils down to the primary question if one should be able to "touch" someone else's task ever at all.We mentioned that if we allow
Task.current
one could potentially store or send it around; This interacts with things like task local storage and the deadline/timeout APIs...Or should we stick to "task is a concept" and all interaction with it is via static functions? It is more correct I guess, i.e. we can then easily disallow any other task being able to set/get task local storage (if it was an API on the task object it could be abused).
So... back to namespace or there's reason to have it a class?