Skip to content

Commit 4886bd5

Browse files
phauslerDougGregorheckj
authored
[Concurrency] AsyncStream and AsyncThrowingStream
* Rework YieldingContinuation to service values in a buffered fashion * Fix word size calculation for locks * Handle terminal states and finished/failed storage * Wrap yielding continuation into a more featureful type for better ergonomics * Hope springs eternal, maybe windows works with this? * Prevent value overflows at .max limits * Add a cancellation handler * Fix series tests missing continuation parameters * Fix series tests for mutable itertaors * Rename to a more general name for Series's inner continuation type * Whitespace fixes and add more commentary about public functions on Series * Restore YieldingContinuation for now with deprecations to favor Series * Ensure onCancel is invoked in deinit phases, and eliminate a potential for double cancellation * Make sure ThrowingSeries has the same nonmutating setter for onCancel as Series * Add a swath of more unit tests that exersize cancellation behavior as well as throwing behaviors * Remove work-around for async testing * Fixup do/catch range to properly handle ThrowingSeries test * Address naming consistency of resume result function * Adopt the async main test setup * More migration of tests to new async mechanisms * Handle the double finish/throw case * Ensure the dependency on Dispatch is built for the series tests (due to semaphore usage) * Add import-libdispatch to run command for Series tests * Use non-combine based timeout intervals (portable to linux) for dispatch semaphore * Rename Series -> AsyncStream and resume functions to just yield, and correct a missing default Element.self value * Fix missing naming change issue for yielding an error on AsyncThrowingStream * Remove argument label of buffering from tests * Extract buffer and throwing variants into their own file * Slightly refactor for only needing to store the producer instead of producer and cancel * Rename onCancel to onTermination * Convert handler access into a function pair * Add finished states to the termination handler event pipeline and a disambiguation enum to identify finish versus cancel * Ensure all termination happens before event propigation (and outside of the locks) and warn against requirements for locking on terminate and enqueue * Modified to use Deque to back the storage and move the storage to inner types; overall perf went from 200kE/sec to over 1ME/sec * Update stdlib/public/Concurrency/AsyncStream.swift Co-authored-by: Doug Gregor <[email protected]> * Update stdlib/public/Concurrency/AsyncThrowingStream.swift Co-authored-by: Doug Gregor <[email protected]> * Update stdlib/public/Concurrency/AsyncStream.swift Co-authored-by: Joseph Heck <[email protected]> * Update stdlib/public/Concurrency/AsyncThrowingStream.swift Co-authored-by: Joseph Heck <[email protected]> * Update stdlib/public/Concurrency/AsyncThrowingStream.swift Co-authored-by: Joseph Heck <[email protected]> * Update stdlib/public/Concurrency/AsyncThrowingStream.swift Co-authored-by: Joseph Heck <[email protected]> * Update stdlib/public/Concurrency/AsyncStream.swift Co-authored-by: Joseph Heck <[email protected]> * Update stdlib/public/Concurrency/AsyncThrowingStream.swift Co-authored-by: Joseph Heck <[email protected]> * Remove local cruft for overlay disabling * Remove local cruft for Dispatch overlay work * Remove potential ABI impact for adding Deque Co-authored-by: Doug Gregor <[email protected]> Co-authored-by: Joseph Heck <[email protected]>
1 parent 93eae81 commit 4886bd5

File tree

9 files changed

+1664
-274
lines changed

9 files changed

+1664
-274
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===--- AsyncStream.cpp - Multi-resume locking interface -----------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 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+
#include "swift/Runtime/Mutex.h"
14+
15+
namespace swift {
16+
// return the size in words for the given mutex primitive
17+
extern "C"
18+
size_t _swift_async_stream_lock_size() {
19+
size_t words = sizeof(MutexHandle) / sizeof(void *);
20+
if (words < 1) { return 1; }
21+
return words;
22+
}
23+
24+
extern "C"
25+
void _swift_async_stream_lock_init(MutexHandle &lock) {
26+
MutexPlatformHelper::init(lock);
27+
}
28+
29+
extern "C"
30+
void _swift_async_stream_lock_lock(MutexHandle &lock) {
31+
MutexPlatformHelper::lock(lock);
32+
}
33+
34+
extern "C"
35+
void _swift_async_stream_lock_unlock(MutexHandle &lock) {
36+
MutexPlatformHelper::unlock(lock);
37+
}
38+
39+
};
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2020-2021 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+
15+
/// An ordered, asynchronously generated sequence of elements.
16+
///
17+
/// AsyncStream is an interface type to adapt from code producing values to an
18+
/// asynchronous context iterating them. This is itended to be used to allow
19+
/// callback or delegation based APIs to participate with async/await.
20+
///
21+
/// When values are produced from a non async/await source there is a
22+
/// consideration that must be made on behavioral characteristics of how that
23+
/// production of values interacts with the iteration. AsyncStream offers a
24+
/// initialization strategy that provides a method of yielding values into
25+
/// iteration.
26+
///
27+
/// AsyncStream can be initialized with the option to buffer to a given limit.
28+
/// The default value for this limit is Int.max. The buffering is only for
29+
/// values that have yet to be consumed by iteration. Values can be yielded in
30+
/// case to the continuation passed into the build closure. That continuation
31+
/// is Sendable, in that it is intended to be used from concurrent contexts
32+
/// external to the iteration of the AsyncStream.
33+
///
34+
/// A trivial use case producing values from a detached task would work as such:
35+
///
36+
/// let digits = AsyncStream(Int.self) { continuation in
37+
/// detach {
38+
/// for digit in 0..<10 {
39+
/// continuation.yield(digit)
40+
/// }
41+
/// continuation.finish()
42+
/// }
43+
/// }
44+
///
45+
/// for await digit in digits {
46+
/// print(digit)
47+
/// }
48+
///
49+
@available(SwiftStdlib 5.5, *)
50+
public struct AsyncStream<Element> {
51+
public struct Continuation: Sendable {
52+
public enum Termination {
53+
case finished
54+
case cancelled
55+
}
56+
57+
let storage: _Storage
58+
59+
/// Resume the task awaiting the next iteration point by having it return
60+
/// nomally from its suspension point or buffer the value if no awaiting
61+
/// next iteration is active.
62+
///
63+
/// - Parameter value: The value to yield from the continuation.
64+
///
65+
/// This can be called more than once and returns to the caller immediately
66+
/// without blocking for any awaiting consuption from the iteration.
67+
public func yield(_ value: __owned Element) {
68+
storage.yield(value)
69+
}
70+
71+
/// Resume the task awaiting the next iteration point by having it return
72+
/// nil which signifies the end of the iteration.
73+
///
74+
/// Calling this function more than once is idempotent; i.e. finishing more
75+
/// than once does not alter the state beyond the requirements of
76+
/// AsyncSequence; which claims that all values past a terminal state are
77+
/// nil.
78+
public func finish() {
79+
storage.finish()
80+
}
81+
82+
/// A callback to invoke when iteration of a AsyncStream is cancelled.
83+
///
84+
/// If an `onTermination` callback is set, when iteration of a AsyncStream is
85+
/// cancelled via task cancellation that callback is invoked. The callback
86+
/// is disposed of after any terminal state is reached.
87+
///
88+
/// Cancelling an active iteration will first invoke the onTermination callback
89+
/// and then resume yeilding nil. This means that any cleanup state can be
90+
/// emitted accordingly in the cancellation handler
91+
public var onTermination: (@Sendable (Termination) -> Void)? {
92+
get {
93+
return storage.getOnTermination()
94+
}
95+
nonmutating set {
96+
storage.setOnTermination(newValue)
97+
}
98+
}
99+
}
100+
101+
let produce: () async -> Element?
102+
103+
/// Construct a AsyncStream buffering given an Element type.
104+
///
105+
/// - Parameter elementType: The type the AsyncStream will produce.
106+
/// - Parameter maxBufferedElements: The maximum number of elements to
107+
/// hold in the buffer past any checks for continuations being resumed.
108+
/// - Parameter build: The work associated with yielding values to the AsyncStream.
109+
///
110+
/// The maximum number of pending elements limited by dropping the oldest
111+
/// value when a new value comes in if the buffer would excede the limit
112+
/// placed upon it. By default this limit is unlimited.
113+
///
114+
/// The build closure passes in a Continuation which can be used in
115+
/// concurrent contexts. It is thread safe to send and finish; all calls are
116+
/// to the continuation are serialized, however calling this from multiple
117+
/// concurrent contexts could result in out of order delivery.
118+
public init(
119+
_ elementType: Element.Type = Element.self,
120+
maxBufferedElements limit: Int = .max,
121+
_ build: (Continuation) -> Void
122+
) {
123+
let storage: _Storage = .create(limit: limit)
124+
produce = storage.next
125+
build(Continuation(storage: storage))
126+
}
127+
}
128+
129+
@available(SwiftStdlib 5.5, *)
130+
extension AsyncStream: AsyncSequence {
131+
/// The asynchronous iterator for iterating a AsyncStream.
132+
///
133+
/// This type is specificially not Sendable. It is not intended to be used
134+
/// from multiple concurrent contexts. Any such case that next is invoked
135+
/// concurrently and contends with another call to next is a programmer error
136+
/// and will fatalError.
137+
public struct Iterator: AsyncIteratorProtocol {
138+
let produce: () async -> Element?
139+
140+
/// The next value from the AsyncStream.
141+
///
142+
/// When next returns nil this signifies the end of the AsyncStream. Any such
143+
/// case that next is invoked concurrently and contends with another call to
144+
/// next is a programmer error and will fatalError.
145+
///
146+
/// If the task this iterator is running in is canceled while next is
147+
/// awaiting a value, this will terminate the AsyncStream and next may return nil
148+
/// immediately (or will return nil on subseuqent calls)
149+
public mutating func next() async -> Element? {
150+
await produce()
151+
}
152+
}
153+
154+
/// Construct an iterator.
155+
public func makeAsyncIterator() -> Iterator {
156+
return Iterator(produce: produce)
157+
}
158+
}
159+
160+
@available(SwiftStdlib 5.5, *)
161+
extension AsyncStream.Continuation {
162+
/// Resume the task awaiting the next iteration point by having it return
163+
/// normally from its suspension point or buffer the value if no awaiting
164+
/// next iteration is active.
165+
///
166+
/// - Parameter result: A result to yield from the continuation.
167+
///
168+
/// This can be called more than once and returns to the caller immediately
169+
/// without blocking for any awaiting consuption from the iteration.
170+
public func yield(
171+
with result: Result<Element, Never>
172+
) {
173+
switch result {
174+
case .success(let val):
175+
storage.yield(val)
176+
}
177+
}
178+
179+
/// Resume the task awaiting the next iteration point by having it return
180+
/// normally from its suspension point or buffer the value if no awaiting
181+
/// next iteration is active where the `Element` is `Void`.
182+
///
183+
/// This can be called more than once and returns to the caller immediately
184+
/// without blocking for any awaiting consuption from the iteration.
185+
public func yield() where Element == Void {
186+
storage.yield(())
187+
}
188+
}

0 commit comments

Comments
 (0)