Skip to content

Commit fe66c47

Browse files
committed
[docs] Provide more documentation on custom executors with global actors
It could be confusing to adopters who were led to believe by the types that they should "just" implement the sharedUnownedExecutor property, but insead they have to implement the unownedExecutor on the specific actor type. Adding documentation clarify this as well as a simple test that exercises this explicitly; We seem to have much coverage of main actor, but not so much of custom executor global actors.
1 parent 12f402f commit fe66c47

File tree

3 files changed

+137
-3
lines changed

3 files changed

+137
-3
lines changed

stdlib/public/Concurrency/GlobalActor.swift

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ import Swift
2222
/// such a declaration from another actor (or from nonisolated code),
2323
/// synchronization is performed through the shared actor instance to ensure
2424
/// mutually-exclusive access to the declaration.
25+
///
26+
/// ## Custom Actor Executors
27+
/// A global actor use a custom executor if it needs to customize its execution
28+
/// semantics, for example, by making sure all of its invocations are run on a
29+
/// specific thread or dispatch queue.
30+
///
31+
/// This is done the same way as with normal non-global actors, by declaring a
32+
/// ``Actor/unownedExecutor`` nonisolated property in the ``ActorType``
33+
/// underlying this global actor.
34+
///
35+
/// It is *not* necessary to override the ``sharedUnownedExecutor`` static
36+
/// property of the global actor, as its default implementation already
37+
/// delegates to the ``shared.unownedExecutor``, which is the most reasonable
38+
/// and correct implementation of this protocol requirement.
39+
///
40+
/// You can find out more about custom executors, by referring to the
41+
/// ``SerialExecutor`` protocol's documentation.
42+
///
43+
/// - SeeAlso: ``SerialExecutor``
2544
@available(SwiftStdlib 5.1, *)
2645
public protocol GlobalActor {
2746
/// The type of the shared actor instance that will be used to provide
@@ -36,10 +55,22 @@ public protocol GlobalActor {
3655
/// instance.
3756
static var shared: ActorType { get }
3857

39-
/// The shared executor instance that will be used to provide
40-
/// mutually-exclusive access for the global actor.
58+
/// Shorthand for referring to the `shared.unownedExecutor` of this global actor.
59+
///
60+
/// When declaring a global actor with a custom executor, prefer to implement
61+
/// the underlying actor's ``Actor/unownedExecutor`` property, and leave this
62+
/// `sharedUnownedExecutor` default implementation in-place as it will simply
63+
/// delegate to the `shared.unownedExecutor`.
64+
///
65+
/// The value of this property must be equivalent to `shared.unownedExecutor`,
66+
/// as may be used by the Swift concurrency runtime or explicit user code. with
67+
/// that assumption in mind.
68+
///
69+
/// Returning different executors for different invocations of this computed
70+
/// property is also illegal, as it could lead to inconsistent synchronization
71+
/// of the underlying actor.
4172
///
42-
/// The value of this property must be equivalent to `shared.unownedExecutor`.
73+
/// - SeeAlso: ``SerialExecutor``
4374
static var sharedUnownedExecutor: UnownedSerialExecutor { get }
4475
}
4576

test/Concurrency/Runtime/custom_executors.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ actor Custom {
2727
}
2828

2929
func report() async {
30+
simple.preconditionIsolated() // we're supposed to be on the same executor as 'simple'
31+
3032
print("custom.count == \(count)")
3133
count += 1
3234

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// RUN: %target-run-simple-swift( %import-libdispatch -strict-concurrency=complete -parse-as-library) | %FileCheck %s
2+
3+
// REQUIRES: concurrency
4+
// REQUIRES: executable_test
5+
// REQUIRES: libdispatch
6+
// UNSUPPORTED: freestanding
7+
8+
// UNSUPPORTED: back_deployment_runtime
9+
// REQUIRES: concurrency_runtime
10+
11+
import Dispatch
12+
13+
let globalQueue = DispatchQueue(label: "SimpleQueue")
14+
15+
@available(SwiftStdlib 6.0, *)
16+
final class NaiveQueueExecutor: SerialExecutor {
17+
public func enqueue(_ unowned: UnownedJob) {
18+
globalQueue.sync {
19+
unowned.runSynchronously(on: self.asUnownedSerialExecutor())
20+
}
21+
}
22+
23+
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
24+
return UnownedSerialExecutor(ordinary: self)
25+
}
26+
27+
func checkIsolated() {
28+
// ok
29+
}
30+
}
31+
32+
@available(SwiftStdlib 6.0, *)
33+
actor Simple {
34+
var count = 0
35+
let exec = NaiveQueueExecutor()
36+
37+
func report() {
38+
print("simple.count == \(count)")
39+
count += 1
40+
}
41+
42+
nonisolated var unownedExecutor: UnownedSerialExecutor {
43+
print("Simple.unownedExecutor")
44+
return exec.asUnownedSerialExecutor()
45+
}
46+
}
47+
48+
@globalActor
49+
@available(SwiftStdlib 6.0, *)
50+
actor MyGlobalActor {
51+
static let simple = Simple()
52+
static let shared = MyGlobalActor()
53+
54+
static var sharedUnownedExecutor: UnownedSerialExecutor {
55+
print("MyGlobalActor.sharedUnownedExecutor")
56+
return simple.unownedExecutor
57+
}
58+
nonisolated var unownedExecutor: UnownedSerialExecutor {
59+
print("MyGlobalActor.unownedExecutor")
60+
return Self.simple.unownedExecutor
61+
}
62+
}
63+
64+
@MyGlobalActor
65+
@available(SwiftStdlib 6.0, *)
66+
final class Custom {
67+
var count = 0
68+
let simple = MyGlobalActor.simple
69+
70+
nonisolated var unownedExecutor: UnownedSerialExecutor {
71+
return simple.unownedExecutor
72+
}
73+
74+
func report() async {
75+
simple.preconditionIsolated()
76+
77+
print("custom.count == \(count)")
78+
count += 1
79+
80+
await simple.report()
81+
}
82+
}
83+
84+
@available(SwiftStdlib 6.0, *)
85+
@main struct Main {
86+
static func main() async {
87+
print("begin")
88+
let actor = Custom()
89+
await actor.report()
90+
print("end")
91+
}
92+
}
93+
94+
// CHECK: begin
95+
// CHECK-NEXT: MyGlobalActor.unownedExecutor
96+
// CHECK-NEXT: Simple.unownedExecutor
97+
// CHECK-NEXT: Simple.unownedExecutor
98+
// CHECK-NEXT: custom.count == 0
99+
// CHECK-NEXT: Simple.unownedExecutor
100+
// CHECK-NEXT: simple.count == 0
101+
// CHECK-NEXT: end

0 commit comments

Comments
 (0)