nonisolated 동기 함수: 호출한 Caller의 액터 컨텍스트를 그대로 이어받아 실행 (동일한 actor executor에서 실행)
nonisolated 비동기 함수: 호출한 쪽이 어떤 액터인지에 관계없이 Global Generic Executor로 컨텍스트 강제 전환
비동기 함수더라도 호출한 액터의 Executor 위에서 그대로 실행되도록 보장
컨텍스트 유지: 함수가 시작, 일시 중단, 재개 될 때 모두 호출자의 액터 컨텍스트 내에서 실행
격리 경계 없음: 호출자와 함수가 동일한 액터 공간을 공유하므로, 인자나 결과값이 격리 경계를 넘지 X
결과적으로 Sendable 구조체가 아니거나 영역 격리(Region Isolation)를 통과하지 못하는 객체도 아무런 경고 없이 안전하게 전달 가능
import Foundation
// Sendable을 따르지 않는 일반 클래스
class RegularUser {
var name: String
init(name: String) { self.name = name }
}
// [기존 방식] 글로벌 실행기로 컨텍스트가 전환되는 비동기 함수
// (6.2 이전의 기본 동작 방식 @concurrent)
@concurrent func oldAsyncFunction(_ user: RegularUser) async {
// 실행되자마자 글로벌 스레드 풀로 이동
try? await Task.sleep(for: .seconds(0.1))
print(user.name)
}
// [Swift 6.2] 호출자의 컨텍스트를 그대로 유지하는 비동기 함수
nonisolated(nonsending) func newAsyncFunction(_ user: RegularUser) async {
// 비동기 함수이지만 글로벌 스레드 풀로 이동하지 않고, 호출한 액터 위에서 그대로 실행
try? await Task.sleep(for: .seconds(0.1))
print(user.name)
}
actor Dashboard {
var user = RegularUser(name: "Chul")
func runTasks() async {
// ❌ 컴파일 에러 발생
// 이유: oldAsyncFunction은 글로벌 스레드 풀로 넘어가기 때문에
// Non-Sendable 객체인 'user'를 안전하게 넘기기 불가
await oldAsyncFunction(user)
// newAsyncFunction은 Dashboard 액터의 스레드 공간 안에서 그대로 실행
// 격리 경계를 넘어가지 않으므로 Non-Sendable 객체를 그대로 전달해도 안전
await newAsyncFunction(user)
}
}
프로젝트 설정으로 모든
nonisolated async함수가 기본값으로nonisolated(nonsending)으로 동작하도록 변경
이전처럼 Global Generic Executor로 동작하게 하고 싶다면?
해당 함수는 호출되는 액터로부터 항상 벗어나 실행되어야 함을 명시하는 키워드
@concurrent
func concurrentAsync(_ data: MyClass) async { }
Task { @concurrent in }
두 개 모두 액터 격리에서 벗어나 Global Generic Executor에서 실행된다는 점은 동일.
호출한 부모로부터 무엇을 상속받는가의 차이가 있음.
Task { @concurrent in }: 호출자의 액터 Executor 상속만 명시적으로 거부하고, Task-Local 값이나 우선순위 등 다른 태스크 컨텍스트는 그대로 상속
Task.detached { }: 호출한 컨텍스트와 완전히 단절된 독립적인 최상위 태스크를 생성. 액터 격리는 물론, Task-Local 값도 전혀 상속 X
참고
[Swift] Swift 6.2 의 nonisolated(nonsending) 과 @concurrent (naljin)
SE-0461: Async Function Isolation (nonisolated(nonsending))