Skip to content

Commit e087759

Browse files
authored
[Concurrency] Custom executors with move-only Job (#63569)
1 parent 8b2ecdb commit e087759

26 files changed

+556
-73
lines changed

include/swift/ABI/Task.h

+4
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class alignas(2 * alignof(void*)) Job :
136136
return Flags.getPriority();
137137
}
138138

139+
uint32_t getJobId() const {
140+
return Id;
141+
}
142+
139143
/// Given that we've fully established the job context in the current
140144
/// thread, actually start running this job. To establish the context
141145
/// correctly, call swift_job_run or runJobInExecutorContext.

include/swift/AST/Decl.h

+3
Original file line numberDiff line numberDiff line change
@@ -3860,6 +3860,9 @@ class NominalTypeDecl : public GenericTypeDecl, public IterableDeclContext {
38603860
/// Find the 'RemoteCallArgument(label:name:value:)' initializer function.
38613861
ConstructorDecl *getDistributedRemoteCallArgumentInitFunction() const;
38623862

3863+
/// Get the move-only `enqueue(Job)` protocol requirement function on the `Executor` protocol.
3864+
AbstractFunctionDecl *getExecutorOwnedEnqueueFunction() const;
3865+
38633866
/// Collect the set of protocols to which this type should implicitly
38643867
/// conform, such as AnyObject (for classes).
38653868
void getImplicitProtocols(SmallVectorImpl<ProtocolDecl *> &protocols);

include/swift/AST/DiagnosticsSema.def

+5
Original file line numberDiff line numberDiff line change
@@ -6408,6 +6408,11 @@ WARNING(hashvalue_implementation,Deprecation,
64086408
"conform type %0 to 'Hashable' by implementing 'hash(into:)' instead",
64096409
(Type))
64106410

6411+
WARNING(executor_enqueue_unowned_implementation,Deprecation,
6412+
"'Executor.enqueue(UnownedJob)' is deprecated as a protocol requirement; "
6413+
"conform type %0 to 'Executor' by implementing 'func enqueue(Job)' instead",
6414+
(Type))
6415+
64116416
//------------------------------------------------------------------------------
64126417
// MARK: property wrapper diagnostics
64136418
//------------------------------------------------------------------------------

include/swift/AST/KnownProtocols.def

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ PROTOCOL(SIMDScalar)
7676
PROTOCOL(BinaryInteger)
7777
PROTOCOL(FixedWidthInteger)
7878
PROTOCOL(RangeReplaceableCollection)
79+
PROTOCOL(Executor)
7980
PROTOCOL(SerialExecutor)
8081
PROTOCOL(GlobalActor)
8182

include/swift/AST/KnownSDKTypes.def

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ KNOWN_SDK_TYPE_DECL(ObjectiveC, ObjCBool, StructDecl, 0)
3939
// standardized
4040
KNOWN_SDK_TYPE_DECL(Concurrency, UnsafeContinuation, NominalTypeDecl, 2)
4141
KNOWN_SDK_TYPE_DECL(Concurrency, MainActor, NominalTypeDecl, 0)
42+
KNOWN_SDK_TYPE_DECL(Concurrency, Job, StructDecl, 0)
43+
KNOWN_SDK_TYPE_DECL(Concurrency, UnownedJob, StructDecl, 0)
44+
KNOWN_SDK_TYPE_DECL(Concurrency, Executor, NominalTypeDecl, 0)
45+
KNOWN_SDK_TYPE_DECL(Concurrency, SerialExecutor, NominalTypeDecl, 0)
4246
KNOWN_SDK_TYPE_DECL(Concurrency, UnownedSerialExecutor, NominalTypeDecl, 0)
4347

4448
KNOWN_SDK_TYPE_DECL(Concurrency, TaskLocal, ClassDecl, 1)

include/swift/Runtime/Concurrency.h

+5
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,11 @@ bool swift_task_isOnExecutor(
715715
const Metadata *selfType,
716716
const SerialExecutorWitnessTable *wtable);
717717

718+
/// Return the 64bit TaskID (if the job is an AsyncTask),
719+
/// or the 32bits of the job Id otherwise.
720+
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
721+
uint64_t swift_task_getJobTaskId(Job *job);
722+
718723
#if SWIFT_CONCURRENCY_ENABLE_DISPATCH
719724

720725
/// Enqueue the given job on the main executor.

lib/AST/ASTContext.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ ProtocolDecl *ASTContext::getProtocol(KnownProtocolKind kind) const {
11201120
case KnownProtocolKind::GlobalActor:
11211121
case KnownProtocolKind::AsyncSequence:
11221122
case KnownProtocolKind::AsyncIteratorProtocol:
1123+
case KnownProtocolKind::Executor:
11231124
case KnownProtocolKind::SerialExecutor:
11241125
M = getLoadedModule(Id_Concurrency);
11251126
break;

lib/AST/Decl.cpp

+33
Original file line numberDiff line numberDiff line change
@@ -5248,6 +5248,39 @@ VarDecl *NominalTypeDecl::getGlobalActorInstance() const {
52485248
nullptr);
52495249
}
52505250

5251+
AbstractFunctionDecl *
5252+
NominalTypeDecl::getExecutorOwnedEnqueueFunction() const {
5253+
auto &C = getASTContext();
5254+
5255+
auto proto = dyn_cast<ProtocolDecl>(this);
5256+
if (!proto)
5257+
return nullptr;
5258+
5259+
llvm::SmallVector<ValueDecl *, 2> results;
5260+
lookupQualified(getSelfNominalTypeDecl(),
5261+
DeclNameRef(C.Id_enqueue),
5262+
NL_ProtocolMembers,
5263+
results);
5264+
5265+
for (auto candidate: results) {
5266+
// we're specifically looking for the Executor protocol requirement
5267+
if (!isa<ProtocolDecl>(candidate->getDeclContext()))
5268+
continue;
5269+
5270+
if (auto *funcDecl = dyn_cast<AbstractFunctionDecl>(candidate)) {
5271+
if (funcDecl->getParameters()->size() != 1)
5272+
continue;
5273+
5274+
auto params = funcDecl->getParameters();
5275+
if (params->get(0)->getSpecifier() == ParamSpecifier::LegacyOwned) { // TODO: make this Consuming
5276+
return funcDecl;
5277+
}
5278+
}
5279+
}
5280+
5281+
return nullptr;
5282+
}
5283+
52515284
ClassDecl::ClassDecl(SourceLoc ClassLoc, Identifier Name, SourceLoc NameLoc,
52525285
ArrayRef<InheritedEntry> Inherited,
52535286
GenericParamList *GenericParams, DeclContext *Parent,

lib/IRGen/GenMeta.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -6231,6 +6231,7 @@ SpecialProtocol irgen::getSpecialProtocolID(ProtocolDecl *P) {
62316231
case KnownProtocolKind::CxxSequence:
62326232
case KnownProtocolKind::UnsafeCxxInputIterator:
62336233
case KnownProtocolKind::UnsafeCxxRandomAccessIterator:
6234+
case KnownProtocolKind::Executor:
62346235
case KnownProtocolKind::SerialExecutor:
62356236
case KnownProtocolKind::Sendable:
62366237
case KnownProtocolKind::UnsafeSendable:

lib/Sema/TypeCheckConcurrency.cpp

+73
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,79 @@ void swift::diagnoseMissingExplicitSendable(NominalTypeDecl *nominal) {
12251225
}
12261226
}
12271227

1228+
void swift::tryDiagnoseExecutorConformance(ASTContext &C,
1229+
const NominalTypeDecl *nominal,
1230+
ProtocolDecl *proto) {
1231+
assert(proto->isSpecificProtocol(KnownProtocolKind::Executor) ||
1232+
proto->isSpecificProtocol(KnownProtocolKind::SerialExecutor));
1233+
1234+
auto &diags = C.Diags;
1235+
auto module = nominal->getParentModule();
1236+
Type nominalTy = nominal->getDeclaredInterfaceType();
1237+
1238+
// enqueue(_: UnownedJob)
1239+
auto enqueueDeclName = DeclName(C, DeclBaseName(C.Id_enqueue), { Identifier() });
1240+
1241+
FuncDecl *unownedEnqueueRequirement = nullptr;
1242+
FuncDecl *moveOnlyEnqueueRequirement = nullptr;
1243+
for (auto req: proto->getProtocolRequirements()) {
1244+
auto *funcDecl = dyn_cast<FuncDecl>(req);
1245+
if (!funcDecl)
1246+
continue;
1247+
1248+
if (funcDecl->getName() != enqueueDeclName)
1249+
continue;
1250+
1251+
1252+
// look for the first parameter being a Job or UnownedJob
1253+
if (funcDecl->getParameters()->size() != 1)
1254+
continue;
1255+
if (auto param = funcDecl->getParameters()->front()) {
1256+
if (param->getType()->isEqual(C.getJobDecl()->getDeclaredInterfaceType())) {
1257+
assert(moveOnlyEnqueueRequirement == nullptr);
1258+
moveOnlyEnqueueRequirement = funcDecl;
1259+
} else if (param->getType()->isEqual(C.getUnownedJobDecl()->getDeclaredInterfaceType())) {
1260+
assert(unownedEnqueueRequirement == nullptr);
1261+
unownedEnqueueRequirement = funcDecl;
1262+
}
1263+
}
1264+
1265+
// if we found both, we're done here and break out of the loop
1266+
if (unownedEnqueueRequirement && moveOnlyEnqueueRequirement)
1267+
break; // we're done looking for the requirements
1268+
}
1269+
1270+
1271+
auto conformance = module->lookupConformance(nominalTy, proto);
1272+
auto concreteConformance = conformance.getConcrete();
1273+
auto unownedEnqueueWitness = concreteConformance->getWitnessDeclRef(unownedEnqueueRequirement);
1274+
auto moveOnlyEnqueueWitness = concreteConformance->getWitnessDeclRef(moveOnlyEnqueueRequirement);
1275+
1276+
if (auto enqueueUnownedDecl = unownedEnqueueWitness.getDecl()) {
1277+
// Old UnownedJob based impl is present, warn about it suggesting the new protocol requirement.
1278+
if (enqueueUnownedDecl->getLoc().isValid()) {
1279+
diags.diagnose(enqueueUnownedDecl->getLoc(), diag::executor_enqueue_unowned_implementation, nominalTy);
1280+
}
1281+
}
1282+
1283+
if (auto unownedEnqueueDecl = unownedEnqueueWitness.getDecl()) {
1284+
if (auto moveOnlyEnqueueDecl = moveOnlyEnqueueWitness.getDecl()) {
1285+
if (unownedEnqueueDecl && unownedEnqueueDecl->getLoc().isInvalid() &&
1286+
moveOnlyEnqueueDecl && moveOnlyEnqueueDecl->getLoc().isInvalid()) {
1287+
// Neither old nor new implementation have been found, but we provide default impls for them
1288+
// that are mutually recursive, so we must error and suggest implementing the right requirement.
1289+
auto ownedRequirement = C.getExecutorDecl()->getExecutorOwnedEnqueueFunction();
1290+
nominal->diagnose(diag::type_does_not_conform, nominalTy, proto->getDeclaredInterfaceType());
1291+
ownedRequirement->diagnose(diag::no_witnesses,
1292+
getProtocolRequirementKind(ownedRequirement),
1293+
ownedRequirement->getName(),
1294+
proto->getDeclaredInterfaceType(),
1295+
/*AddFixIt=*/true);
1296+
}
1297+
}
1298+
}
1299+
}
1300+
12281301
/// Determine whether this is the main actor type.
12291302
static bool isMainActor(Type type) {
12301303
if (auto nominal = type->getAnyNominal())

lib/Sema/TypeCheckConcurrency.h

+3
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ void diagnoseMissingSendableConformance(
280280
/// state whether it conforms to Sendable, provide a diagnostic.
281281
void diagnoseMissingExplicitSendable(NominalTypeDecl *nominal);
282282

283+
/// Warn about deprecated `Executor.enqueue` implementations.
284+
void tryDiagnoseExecutorConformance(ASTContext &C, const NominalTypeDecl *nominal, ProtocolDecl *proto);
285+
283286
/// How the Sendable check should be performed.
284287
enum class SendableCheck {
285288
/// Sendable conformance was explicitly stated and should be

lib/Sema/TypeCheckProtocol.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -6496,6 +6496,8 @@ void TypeChecker::checkConformancesInContext(IterableDeclContext *idc) {
64966496
} else if (proto->isSpecificProtocol(
64976497
KnownProtocolKind::UnsafeSendable)) {
64986498
sendableConformanceIsUnchecked = true;
6499+
} else if (proto->isSpecificProtocol(KnownProtocolKind::Executor)) {
6500+
tryDiagnoseExecutorConformance(Context, nominal, proto);
64996501
}
65006502
}
65016503

stdlib/public/Concurrency/Actor.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ static bool swift_task_isCurrentExecutorImpl(ExecutorRef executor) {
301301
return currentTracking->getActiveExecutor() == executor;
302302
}
303303

304+
// TODO(ktoso): checking the "is main thread" is not correct, main executor can be not main thread, relates to rdar://106188692
304305
return executor.isMainExecutor() && isExecutingOnMainThread();
305306
}
306307

stdlib/public/Concurrency/Executor.swift

+78-10
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,19 @@ import Swift
1515
/// A service that can execute jobs.
1616
@available(SwiftStdlib 5.1, *)
1717
public protocol Executor: AnyObject, Sendable {
18+
19+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
20+
@available(macOS, introduced: 10.15, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
21+
@available(iOS, introduced: 13.0, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
22+
@available(watchOS, introduced: 6.0, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
23+
@available(tvOS, introduced: 13.0, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
24+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
1825
func enqueue(_ job: UnownedJob)
26+
27+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
28+
@available(SwiftStdlib 5.9, *)
29+
func enqueue(_ job: __owned Job)
30+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
1931
}
2032

2133
/// A service that executes jobs.
@@ -26,13 +38,51 @@ public protocol SerialExecutor: Executor {
2638
// avoid drilling down to the base conformance just for the basic
2739
// work-scheduling operation.
2840
@_nonoverride
41+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
42+
@available(macOS, introduced: 10.15, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
43+
@available(iOS, introduced: 13.0, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
44+
@available(watchOS, introduced: 6.0, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
45+
@available(tvOS, introduced: 13.0, deprecated: 9999, message: "Implement 'enqueue(_: __owned Job)' instead")
46+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
2947
func enqueue(_ job: UnownedJob)
3048

49+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
50+
// This requirement is repeated here as a non-override so that we
51+
// get a redundant witness-table entry for it. This allows us to
52+
// avoid drilling down to the base conformance just for the basic
53+
// work-scheduling operation.
54+
@_nonoverride
55+
@available(SwiftStdlib 5.9, *)
56+
func enqueue(_ job: __owned Job)
57+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
58+
3159
/// Convert this executor value to the optimized form of borrowed
3260
/// executor references.
61+
@available(SwiftStdlib 5.9, *)
3362
func asUnownedSerialExecutor() -> UnownedSerialExecutor
3463
}
3564

65+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
66+
@available(SwiftStdlib 5.9, *)
67+
extension Executor {
68+
public func enqueue(_ job: UnownedJob) {
69+
self.enqueue(Job(job))
70+
}
71+
72+
public func enqueue(_ job: __owned Job) {
73+
self.enqueue(UnownedJob(job))
74+
}
75+
}
76+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
77+
78+
@available(SwiftStdlib 5.9, *)
79+
extension SerialExecutor {
80+
@available(SwiftStdlib 5.9, *)
81+
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
82+
UnownedSerialExecutor(ordinary: self)
83+
}
84+
}
85+
3686
/// An unowned reference to a serial executor (a `SerialExecutor`
3787
/// value).
3888
///
@@ -88,14 +138,6 @@ public struct UnownedSerialExecutor: Sendable {
88138
@_silgen_name("swift_task_isOnExecutor")
89139
public func _taskIsOnExecutor<Executor: SerialExecutor>(_ executor: Executor) -> Bool
90140

91-
// Used by the concurrency runtime
92-
@available(SwiftStdlib 5.1, *)
93-
@_silgen_name("_swift_task_enqueueOnExecutor")
94-
internal func _enqueueOnExecutor<E>(job: UnownedJob, executor: E)
95-
where E: SerialExecutor {
96-
executor.enqueue(job)
97-
}
98-
99141
@available(SwiftStdlib 5.1, *)
100142
@_transparent
101143
public // COMPILER_INTRINSIC
@@ -109,7 +151,33 @@ func _checkExpectedExecutor(_filenameStart: Builtin.RawPointer,
109151
}
110152

111153
_reportUnexpectedExecutor(
112-
_filenameStart, _filenameLength, _filenameIsASCII, _line, _executor)
154+
_filenameStart, _filenameLength, _filenameIsASCII, _line, _executor)
155+
}
156+
157+
/// Primarily a debug utility.
158+
///
159+
/// If the passed in Job is a Task, returns the complete 64bit TaskId,
160+
/// otherwise returns only the job's 32bit Id.
161+
///
162+
/// - Returns: the Id stored in this Job or Task, for purposes of debug printing
163+
@available(SwiftStdlib 5.9, *)
164+
@_silgen_name("swift_task_getJobTaskId")
165+
internal func _getJobTaskId(_ job: UnownedJob) -> UInt64
166+
167+
// Used by the concurrency runtime
168+
@available(SwiftStdlib 5.1, *)
169+
@_silgen_name("_swift_task_enqueueOnExecutor")
170+
internal func _enqueueOnExecutor<E>(job unownedJob: UnownedJob, executor: E)
171+
where E: SerialExecutor {
172+
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
173+
if #available(SwiftStdlib 5.9, *) {
174+
executor.enqueue(Job(context: unownedJob._context))
175+
} else {
176+
executor.enqueue(unownedJob)
177+
}
178+
#else // SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
179+
executor.enqueue(unownedJob)
180+
#endif // !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
113181
}
114182

115183
#if !SWIFT_STDLIB_SINGLE_THREADED_CONCURRENCY && !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
@@ -139,4 +207,4 @@ internal final class DispatchQueueShim: @unchecked Sendable, SerialExecutor {
139207
return UnownedSerialExecutor(ordinary: self)
140208
}
141209
}
142-
#endif
210+
#endif // SWIFT_STDLIB_SINGLE_THREADED_CONCURRENCY

stdlib/public/Concurrency/ExecutorAssertions.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import SwiftShims
3434
/// programming error.
3535
///
3636
/// - Parameter executor: the expected current executor
37-
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
37+
@available(SwiftStdlib 5.9, *)
3838
public
3939
func preconditionTaskOnExecutor(
4040
_ executor: some SerialExecutor,
@@ -71,7 +71,7 @@ func preconditionTaskOnExecutor(
7171
/// programming error.
7272
///
7373
/// - Parameter actor: the actor whose serial executor we expect to be the current executor
74-
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
74+
@available(SwiftStdlib 5.9, *)
7575
public
7676
func preconditionTaskOnActorExecutor(
7777
_ actor: some Actor,
@@ -108,7 +108,7 @@ func preconditionTaskOnActorExecutor(
108108
/// assumption is a serious programming error.
109109
///
110110
/// - Parameter executor: the expected current executor
111-
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
111+
@available(SwiftStdlib 5.9, *)
112112
public
113113
func assertTaskOnExecutor(
114114
_ executor: some SerialExecutor,
@@ -143,7 +143,7 @@ func assertTaskOnExecutor(
143143
///
144144
///
145145
/// - Parameter actor: the actor whose serial executor we expect to be the current executor
146-
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
146+
@available(SwiftStdlib 5.9, *)
147147
public
148148
func assertTaskOnActorExecutor(
149149
_ actor: some Actor,
@@ -180,7 +180,7 @@ func assertTaskOnActorExecutor(
180180
/// if another actor uses the same serial executor--by using ``MainActor/sharedUnownedExecutor``
181181
/// as its own ``Actor/unownedExecutor``--this check will succeed, as from a concurrency safety
182182
/// perspective, the serial executor guarantees mutual exclusion of those two actors.
183-
@available(SwiftStdlib 5.9, *) // FIXME: use @backDeploy(before: SwiftStdlib 5.9)
183+
@available(SwiftStdlib 5.9, *)
184184
@_unavailableFromAsync(message: "await the call to the @MainActor closure directly")
185185
public
186186
func assumeOnMainActorExecutor<T>(

0 commit comments

Comments
 (0)