Skip to content

[Concurrency] Offer way to get SerialExecutor from Actor #81063

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 4 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions stdlib/public/Concurrency/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,31 @@ public protocol SchedulableExecutor: Executor {

}

extension Actor {

/// Perform an operation with the actor's ``SerialExecutor``.
///
/// This converts the actor's ``Actor/unownedExecutor`` to a ``SerialExecutor`` while
/// retaining the actor for the duration of the operation. This is to ensure the lifetime
/// of the executor while performing the operation.
@_alwaysEmitIntoClient
@available(SwiftStdlib 5.1, *)
public nonisolated func withSerialExecutor<T: ~Copyable, E: Error>(_ operation: (any SerialExecutor) throws(E) -> T) throws(E) -> T {
try operation(unsafe unsafeBitCast(self.unownedExecutor, to: (any SerialExecutor).self))
}

/// Perform an operation with the actor's ``SerialExecutor``.
///
/// This converts the actor's ``Actor/unownedExecutor`` to a ``SerialExecutor`` while
/// retaining the actor for the duration of the operation. This is to ensure the lifetime
/// of the executor while performing the operation.
@_alwaysEmitIntoClient
@available(SwiftStdlib 5.1, *)
public nonisolated func withSerialExecutor<T: ~Copyable, E: Error>(_ operation: nonisolated(nonsending) (any SerialExecutor) async throws(E) -> T) async throws(E) -> T {
try await operation(unsafe unsafeBitCast(self.unownedExecutor, to: (any SerialExecutor).self))
}
}

extension Executor {
/// Return this executable as a SchedulableExecutor, or nil if that is
/// unsupported.
Expand Down
103 changes: 103 additions & 0 deletions test/Concurrency/Runtime/actor_isIsolatingCurrentContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift %import-libdispatch -Xfrontend -disable-availability-checking -parse-as-library %s -o %t/a.out
// RUN: %target-codesign %t/a.out
// RUN: %target-run %t/a.out | %FileCheck %s

// REQUIRES: executable_test
// REQUIRES: concurrency
// REQUIRES: concurrency_runtime

// REQUIRES: libdispatch

// UNSUPPORTED: back_deployment_runtime
// UNSUPPORTED: back_deploy_concurrency
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: freestanding

@available(SwiftStdlib 6.2, *)
final class IsIsolatingExecutor: SerialExecutor {
init() {}

func enqueue(_ job: consuming ExecutorJob) {
job.runSynchronously(on: self.asUnownedSerialExecutor())
}

func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}

func checkIsolated() {
print("called: checkIsolated")
}

func isIsolatingCurrentContext() -> Bool {
print("called: isIsolatingCurrentContext")
return true
}
}

@available(SwiftStdlib 6.2, *)
final class NoChecksImplementedExecutor: SerialExecutor {
init() {}

func enqueue(_ job: consuming ExecutorJob) {
job.runSynchronously(on: self.asUnownedSerialExecutor())
}

func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}

@available(SwiftStdlib 6.2, *)
final class JustCheckIsolatedExecutor: SerialExecutor {
init() {}

func enqueue(_ job: consuming ExecutorJob) {
job.runSynchronously(on: self.asUnownedSerialExecutor())
}

func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}

func checkIsolated() {
print("called: checkIsolated")
}
}

@available(SwiftStdlib 6.2, *)
actor ActorOnIsCheckImplementingExecutor<Ex: SerialExecutor> {
let executor: Ex

init(on executor: Ex) {
self.executor = executor
}

nonisolated var unownedExecutor: UnownedSerialExecutor {
self.executor.asUnownedSerialExecutor()
}

func checkIsIsolatingCurrentContext() async -> Bool {
executor.isIsolatingCurrentContext()
}
}

@main struct Main {
static func main() async {
let hasIsIsolatingCurrentContextExecutor = IsIsolatingExecutor()
let hasIsCheckActor = ActorOnIsCheckImplementingExecutor(on: hasIsIsolatingCurrentContextExecutor)

let anyActor: any Actor = hasIsCheckActor

anyActor.withSerialExecutor { se in
let outside = se.isIsolatingCurrentContext()
assert(outside == true) // This is just a mock executor impl that always returns "true" (it is lying)
// CHECK: called: isIsolatingCurrentContext
}

let inside = await hasIsCheckActor.checkIsIsolatingCurrentContext()
assert(inside == true)
// CHECK: called: isIsolatingCurrentContext
}
}
8 changes: 6 additions & 2 deletions test/IDE/complete_cache_notrecommended.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ func testAsync() async -> Int {
}
func testSyncMember(obj: MyActor) -> Int {
obj.#^MEMBER_IN_SYNC^#
// MEMBER_IN_SYNC: Begin completions, 9 items
// MEMBER_IN_SYNC: Begin completions, 11 items
// MEMBER_IN_SYNC-DAG: Keyword[self]/CurrNominal: self[#MyActor#];
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/CurrNominal/TypeRelation[Convertible]: actorMethod()[' async'][#Int#];
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/CurrNominal/NotRecommended: deprecatedMethod()[' async'][#Void#];
// MEMBER_IN_SYNC-DAG: Decl[InstanceVar]/CurrNominal: unownedExecutor[#UnownedSerialExecutor#];
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: withSerialExecutor({#(operation): (any SerialExecutor) throws(Error) -> ~Copyable##(any SerialExecutor) throws(Error) -> ~Copyable#})[' throws'][#~Copyable#]; name=withSerialExecutor(:)
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: withSerialExecutor({#(operation): (any SerialExecutor) async throws(Error) -> ~Copyable##(any SerialExecutor) async throws(Error) -> ~Copyable#})[' async'][' throws'][#~Copyable#]; name=withSerialExecutor(:)
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: preconditionIsolated()[#Void#]; name=preconditionIsolated()
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: preconditionIsolated({#(message): String#})[#Void#]; name=preconditionIsolated(:)
// MEMBER_IN_SYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: assertIsolated()[#Void#]; name=assertIsolated()
Expand All @@ -46,11 +48,13 @@ func testSyncMember(obj: MyActor) -> Int {

func testSyncMember(obj: MyActor) async -> Int {
obj.#^MEMBER_IN_ASYNC^#
// MEMBER_IN_ASYNC: Begin completions, 9 items
// MEMBER_IN_ASYNC: Begin completions, 11 items
// MEMBER_IN_ASYNC-DAG: Keyword[self]/CurrNominal: self[#MyActor#];
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/CurrNominal/TypeRelation[Convertible]: actorMethod()[' async'][#Int#];
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/CurrNominal/NotRecommended: deprecatedMethod()[' async'][#Void#];
// MEMBER_IN_ASYNC-DAG: Decl[InstanceVar]/CurrNominal: unownedExecutor[#UnownedSerialExecutor#];
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: withSerialExecutor({#(operation): (any SerialExecutor) throws(Error) -> ~Copyable##(any SerialExecutor) throws(Error) -> ~Copyable#})[' throws'][#~Copyable#]; name=withSerialExecutor(:)
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: withSerialExecutor({#(operation): (any SerialExecutor) async throws(Error) -> ~Copyable##(any SerialExecutor) async throws(Error) -> ~Copyable#})[' async'][' throws'][#~Copyable#]; name=withSerialExecutor(:)
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: preconditionIsolated()[#Void#]; name=preconditionIsolated()
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: preconditionIsolated({#(message): String#})[#Void#]; name=preconditionIsolated(:)
// MEMBER_IN_ASYNC-DAG: Decl[InstanceMethod]/Super/IsSystem: assertIsolated()[#Void#]; name=assertIsolated()
Expand Down