티스토리 뷰

🍏/Swift

[Swift] @isolated(any)

eungding 2024. 11. 15. 21:51
728x90
반응형

SE-0431 @isolated(any) Function Types 

📝 Assisted by GPT 

 


[1] @isolated(any) 

 

@isolated(any) 은 swift 6 에 추가된 attribute 입니다. (function only)

임의의 (하지만 정적으로 알 수 없는) 격리를 가진 함수를 표현할 수 있습니다. 

 

Task 에도 반영됨! 

 

 



 

[2] @isolated(any)  도입 배경 

 

Task 이니셜라이저는 () async throws -> () 타입의 불투명 값 (opaque value) 을 받습니다. 그러나 이 값에서 격리를 동적으로 복구할 수 없기 때문에, 이니셜라이저는 해당 작업을 global concurrent executor 에서 시작할 수밖에 없습니다. 만약 이니셜라이저에 전달된 함수가 실제로 특정 액터에 격리되어 있다면, 해당 함수는 진입 시 즉시 그 액터로 전환해야 합니다. 이 과정은 추가적인 동기화를 요구하며, 작업을 다시 일시 중단해야 할 수도 있습니다.

더 중요한 점은, 작업이 실제로 액터에 대기열로 추가되는 순서가 작업이 생성된 순서와 반드시 일치하지 않는다는 것입니다. 이니셜라이저가 처음부터 올바른 executor 에 작업을 바로 추가할 수 있다면, 의미적으로나 성능적으로 훨씬 더 나을 것입니다.

이 문제를 해결하기 위한 간단한 방법은 임의의 (하지만 정적으로 알 수 없는) 격리를 가진 함수를 표현할 수 있는 타입을 추가하는 것입니다. 


표준 라이브러리의 모든 task-creation API는 @isolated(any) function 을 받아들이도록 업데이트 되었으며, 새 작업 (task) 를 적절한 executor 에 동기적으로 추가하게 됩니다.

 

 

[3] @isolated(any)  사용하기 

 

함수가 임의의 컨텍스트에서 호출될 때, 항상 isolation boundary 를 넘는다고 가정해야 합니다.

그래서 비동기적으로 해당 함수를 호출해야 합니다.  

 

func doSomething(_ operation: @isolated(any) () -> Void) async {
    await operation()
}

 

 

[4] isolation property

 

@isolated(any) 함수 타입의 값은 isolation 이라는 특별한 프로퍼티를 가집니다.

이 프로퍼티는 read-only 전용이며, 타입은 (any Actor)? 입니다. 

이 값은 함수 값의 동적 격리 (dynamic isolation) 에 의해 결정됩니다. 

 

func doSomething(_ operation: @isolated(any) () -> Void) async {
    let isolation = operation.isolation
    print(isolation) // ex. Optional(Swift.MainActor)
 }

 

 

- 만약 함수가 동적으로 비격리(non-isolated) 상태라면, 격리 값은 nil입니다.
- 만약 함수가 동적으로 글로벌 액터 타입 G에 격리되어 있다면, 격리 값은 G.shared입니다.
- 만약 함수가 동적으로 특정 액터 참조에 격리되어 있다면, 격리 값은 해당 actor reference 입니다.

 

import Foundation

func doSomething(_ operation: @Sendable @escaping @isolated(any) () -> Void) {
    print(operation.isolation)
    
    Task {
        print(operation.isolation)
        await operation()
    }
}

doSomething {
    // 출력 nil
}

doSomething { @MainActor in
    // 출력 Optional(Swift.MainActor)
}

doSomething { @MyGlobalActor in
    // 출력 Optional(__lldb_expr_631.MyGlobalActor)
}
    
@globalActor
actor MyGlobalActor {
    static let shared = MyGlobalActor()
}

 

 

[5] 추후 계획

 

@isolated(any) 함수 값의 격리는 정적으로 알 수 없기 때문에, 일반적으로 그 함수에 대한 호출은 격리 경계를 넘습니다. 이는 호출이 비동기적으로 처리되어야 한다는 것을 의미하며, 함수가 동기적이라 하더라도 await가 필요하고, 인수와 결과는 격리 경계를 넘는 호출에 대한 일반적인 송수신 가능성(sendability) 제약을 충족해야 합니다. 

 

@isolated(any) 함수에 대한 호출이 격리 경계를 넘지 않도록 처리되려면, 호출자는 함수와 동일한 격리를 가져야 한다고 알려져야 합니다. @isolated(any) 매개변수의 격리는 반드시 불투명 값(opaque value)이기 때문에, 호출자가 함수와 동일한 격리 상태를 가져야 하는데, 이는 호출자가 값별 격리(value-specific isolation)로 선언되어야 함을 의미합니다. 현재로서는 로컬 함수나 클로저가 현재 컨텍스트의 격리 외에 특정 값에 격리될 수 있는 방법은 없습니다.


다음 규칙들은 @isolated(any)가 향후 언어 지원과 어떻게 상호작용할지에 대해 설명합니다. 이 규칙들을 제시하기 위해, 이 제안서는 클로저 격리 제어 구상에서 제안된 구문을 사용합니다. 이 구문에서 캡처 목록의 항목에 isolated를 추가하면 클로저가 그 값에 격리되도록 합니다. 

 

func delay(operation: @isolated(any) () -> ()) {
  let isolation = operation.isolation
  Task { [isolated isolation] in // <-- tentative syntax from the isolated captures pitch
    print("waking")
    operation() // <-- does not cross an isolation barrier and so is synchronous
    print("finished")
  }
}

 

 


 

[참고]

 

위 proposal 의 Motivation 쪽에 나오는 내용인데, 같이 읽으면 더 개념이 잡힘. 

 

 # 함수의 3가지 격리 (isolation) 유형 

 

Swift에서 함수 선언과 클로저는 세 가지 형태의 actor 격리를 지원합니다.


1) 비격리(non-isolated)
함수나 클로저가 특정 actor에 격리되지 않고, 동시성 제약 없이 동작하는 경우를 의미합니다.

2) 글로벌 actor 격리
함수나 클로저가 특정 글로벌 actor(예: @MainActor)에 격리되어 해당 actor의 컨텍스트 내에서만 실행되도록 제한됩니다.

3) 특정 매개변수(parameter) 또는 캡처된 값(captured value)에 격리
함수나 클로저가 전달받은 특정 매개변수 또는 클로저가 캡처한 값(예: self)에 대해 격리되어, 해당 값과의 상호작용이 안전하게 이루어지는 경우를 의미합니다.

 

3번은 이런 예제를 말함. 

 

함수는 명시적으로 특정 매개변수를 isolated로 선언하여 그 매개변수에 스스로를 격리시킬 수 있습니다. 이런 격리는 actor 메서드에서 self 매개변수에도 암묵적으로 적용됩니다 (다른 격리를 명시적으로 사용하지 않은 경우).

 

 

 

#  한계 

function 이 직접 호출 될 때 (called directly) , Swift 의 isolation checker 는 해당 함수의 isolation 을 정확히 분석하고 호출 컨텍스트의 격리와 비교할 수 있습니다.  그러나 호출 표현식이 함수 타입의 불투명 값(opaque value)을 호출하는 경우, Swift는 타입 시스템에서 표현할 수 있는 한계에 제약을 받습니다.

 

 타입시스템으로 표현 가능  

 

- A function type with no isolation specifiers, such as () -> Int, represents a non-isolated function.
- A function type with a global actor attribute, such as @MainActor () -> Int, represents a function that's isolated to that global actor.
- A function type with an isolated parameter, such as (isolated MyActor) - > Int, represents a function that's isolated to that parameter.

 

 

타입시스템으로 표현 할 수 없음

 

- 클로저가 캡처한 값 중 하나에 대해 격리될 수 있다는 것

 

아래 예제에서 클로저는 캡처한 self 값에 대해 격리됩니다.

 

actor WorldModelObject {

  ...
  
  func updateLater() {
    Task {
      // This closure doesn't have an explicit isolation
      // specification, and it's being passed to the `Task`
      // initializer, so it will be inferred to have the same
      // isolation as its enclosing context.  The enclosing
      // context is isolated to its `self` parameter, which this
      // closure captures, so this closure will also be isolated
      // that value.
      
      // 이 클로저는 명시적인 격리 지정이 없습니다.
      // 그리고 이 클로저는 `Task`에  전달되고 있으므로,
      // 자신을 둘러싼 컨텍스트와 동일한 격리를 갖는 것으로 추론됩니다.
      // 둘러싼 컨텍스트는 `self` 매개변수에 격리되어 있으며,
      // 이 클로저가 이를 캡처하고 있으므로, 이 클로저 역시
      // 해당 값에 격리됩니다.
      self.update()
    }
  }
}

 

 

upcoming closure isolation control proposal 에서는 이 개념이 훨씬 더 중요해질 것으로 예상됩니다.

이 제안에 따르면, 격리된 캡처는 특정 코드 조각의 격리를 제어하는 강력한 일반 도구가 될 것입니다. 하지만 여전히 해당 클로저의 격리를 타입 시스템으로 표현할 방법은 제공되지 않을 것입니다.

 

 

 

 

[ 추천 ]

 

-  노션  (위의 proposal 을 쉽게 번역+정리 해주신 링크 발견 ✨) 

 

 

 

반응형

'🍏 > Swift' 카테고리의 다른 글

[Swift] 매크로 (Macro)  (1) 2024.12.07
[Swift] @unchecked, @preconcurrency, @retroactive  (9) 2024.11.06
[Swift] Noncopyable (~Copyable)  (3) 2024.10.26
[Swift] Typed throws  (0) 2024.10.13
[Swift] AsyncStream  (1) 2024.08.24
댓글