iOS/Swift Concurrency

[스터디 숙제] region based isolation

Hamp 2025. 9. 6. 19:41
반응형

👋 들어가기 전

3번 째 스터디숙제는 바로 region based isolated다.

 

다소 생소한 개념이니깐 proposal을 정리해보자..


🏁 학습할 내용

  • Introduction
  • Motivation
  • Propsed Solution

🙋 소개

기존 Swift Concurrency는 값들을  actor / task 의해 결정되는 격리 도메인에 묶인다.

Sendbale이 아닌 값이 격리 경계를 절대 넘을 수 없다.

 

이런 특징은 데이터 레이스를 방지할 수 있는 큰 장점도 있지만, 반대로 말하면 너무 제약이 강해,

실제로 안전한 코드까지 막을 수 있는 단점이 있다.

 

Sendable 이 문서에서는 비격리 값이 격리 경계를 넘어 안전하게 전송될 수 있는지 여부를 판단하는 새로운 제어 흐름 감지 진단을 도입하여 이러한 규칙을 완화할 것을 제안한다 .

 

이는 격리 영역(isolation region) 이라는 개념을 도입하여 컴파일러가 두 값이 서로 영향을 미칠 수 있는지 보수적으로 추론할 수 있도록 한다.

 

격리 영역을 사용하면 언어는 비격리 값을 Sendable 격리 경계를 넘어 전송해도 경합이 발생하지 않음을 증명할 수 있다.

전송 지점 이후 호출자에서 해당 값(및 해당 값을 참조하는 다른 값)이 사용되지 않으면 된다.


🎉 등장 계기

아래 코드를 살펴보자.

// ❌ Sendable 아님
class Client {
  init(name: String, initialBalance: Double) { ... }
}

actor ClientStore {
  var clients: [Client] = []

  static let shared = ClientStore()

  func addClient(_ c: Client) {
    clients.append(c)
  }
}

func openNewAccount(name: String, initialBalance: Double) async {
  let client = Client(name: name, initialBalance: initialBalance)
  await ClientStore.shared.addClient(client) 
  // 🚨 에러: Client는 Sendable이 아님
}

 

🧐 코드 분석

  • Client는 클래스 타입이기 때문에, 기본적으로 Sendbale이 아님
  • actor(ClientStore)의 격리된 함수 addClient(_:)에 넘기려니 컴파일러가 막음

 

⁉️ 왜 막힌걸까?

  • SE-03202 규칙에 따르면 Sendable이  아닌 값은 isolation boundary(격리 경계)를 절대 넘어갈 수 없다.
  • Sendable 자체가 가변 상태에 대한 데이터 레이스를 막기위함이니깐

 

🤣 그런데 사실 안전했다?

  • clientString, Double만 받아서 초기화됨 → 둘 다 Sendable.
  • client는 방금 생성된 참조라서, 다른 스레드/actor에서 아직 접근할 방법이 없음.
  • clientopenNewAccount 함수 안에서 addClient 호출 이후 더 이상 사용되지 않음.

즉, 실제로는 데이터 레이스가 전혀 발생할 수 없다.

 

🚨 문제점

  • SE-03032 규칙이 너무 보수적이라서, 실제로는 안전하지만, 격리 경계를 못 넘어감
  • 그래서 @unchekd Sendable과 같은 임시 탈출구를 써야했었음
  • 하지만, 이건 타입 전체를 무조건 Sendable로 만들어야해서, 오히려 더 많은 리소스가 들어갈 문제가 있음

⭐️ 제안된 해결책

 

⛓️ 격리 공간 등장

 

1️⃣ isolation Region(격리 공간)

  • 특정 non-Sendable 값들이 보여있는 집합
  • 집합 안의 값들은 오직 같은 집합 안의 다른 값들을 통해서만 참조 될 수 있다.

같은 격리 공간(P) 에 있다는 것은 아래와 같은 특징을 의미한다.

  • x와 y값이 alias(동일한 참조를 공유)할 수 있거나
  • x 또는 x의 속성이 y의 속성 접근(체이닝 property 접근)을 통해 참조 될 수 있는 경우

이 특징 덕분에, 서로 다른 isolation region에 있는 non-Sendable 값들을 동시에 사용될 수 있다.

 

2️⃣ isolation Domain(격리 도메인)

  • isolation Region(격리 공간)이 속할 수 있는 더 큰 맥락
    • Task
    • actor
    • 아무곳도 속하지 않은 상태

 

3️⃣ 표기법

  • [(a)]
    • a가 혼자 있는 독립 region
  • [{(a), actorInstance}]
    • aactorInstance의 격리 도메인에 속한 region
  • [(a), {(b), actorInstance}]
    • a는 독립, bactor
  • [{((x,y), @OtherActor}, (z), (w,t)]
    • x, y@OtherActor에 속한 같은 region
    • z는 독립 region
    • w, t는 또 다른 독립 region

 

4️⃣ 병합 규칙

 

region은 함수 호출, 대입(할당) 같은 alias 가능성이 생길 때, 합쳐진다.

 

인자 병합

  • non-Sendable 인자가 여러 개 있으면 → 전부 같은 region으로 합쳐짐
func bindingInitialization() {
  let x = NonSendable()
  // Regions: [(x)]
  let y = x
  // Regions: [(x, y)]
  let z = consume x
  // Regions: [(x, y, z)]
}

 

  • var binding은  mutable하기 때문에, 새로운 값으로 대입되면 이전 alias는 깨지고, 새로운 값과 alias관계를 형성함
func mutableBindingAssignmentSimple() {
  var x = NonSendable()
  // Regions: [(x)]
  let y = NonSendable()
  // Regions: [(x), (y)]
  x = y
  // Regions: [(x, y)]
  let z = NonSendable()
  // Regions: [(x, y), (z)]
  x = z
  // Regions: [(y), (x, z)]
}

 

  • 만약 closure에 의해 캡쳐됐다면, alias 관계를 유지해야하므로 옛 region도 보존
// Since we pass x as inout in the closure, the closure has to capture x by
// reference.
func mutableBindingAssignmentClosure() {
  var x = NonSendable()
  // Regions: [(x)]
  let closure = { useInOut(&x) }
  // Regions: [(x, closure)]
  let y = NonSendable()
  // Regions: [(x, closure), (y)]
  x = y
  // Regions: [(x, closure, y)]
}

 

  • Non-Sendable 값의 프로퍼티 접근 시, 같은 Isolation Region으로 묶임
    • property 접근 시 getter가 호출되고 self가 전달되므로

 

func assignFieldToValue() {
  let x = NonSendableStruct()
  // Regions: [(x)]
  let y = x.field
  // Regions: [(x, y)]
}

 

  • Non-Sendable 값의 프로퍼티에 값 할당 시 같은 isolation Region으로 묶임, setter 호출되므로
func assignValueToField() {
  let x = NonSendableStruct()
  // Regions: [(x)]
  let y = NonSendable()
  // Regions: [(x), (y)]
  x.field = y
  // Regions: [(x, y)]
}

 

  • 함수 인자는 non-Sendable일 경우에도 같은 region으로 묶인다.
    • 메서드 호출 시, self가 non-Sendable이면, 모든 메서드 인자는 자동으로 self와 같은 region으로 들어간다.
func transfer(x: NonSendable, y: NonSendable) {
  // Regions: [(x, y)]
  let z = NonSendable()
  
  // 함수 내부에서 새로운 non-Sendable 값을 만들면 처음에는 독립적인 region에 속한다.
  // Regions: [(x, y), (z)]
   
  f(x, z)
  // 하지만 f(x, z)처럼 함께 다른 함수에 전달되면 region이 합쳐져 [(x, y, z)]가 된다.
  // Regions: [(x, y, z)]
}

 

 

예제 코드 🤖

let john = Client(name: "John", initialBalance: 0)
let joanna = Client(name: "Joanna", initialBalance: 0)

await ClientStore.shared.addClient(john)
await ClientStore.shared.addClient(joanna) // (1)

 

1️⃣ 두 Client가 완전히 분리된 경우

let john = Client(name: "John", initialBalance: 0)
let joanna = Client(name: "Joanna", initialBalance: 0)

 

  • jonrhk jonna는 서로 참조할 수 없음, 서로 다른 isolation region에 속함
  • 따라서 ClientStore.shared안에서 john과 joanna를 사용하는 코드가 실행되더라도 데이터 경쟁 발생 없음
  • (1)에서 jonna를 사용한 코드가 ClientStore.shared 안의 john 접근 코드와 동시에 실행되도 안전

 

2️⃣  참조가 생긴 경우

john.friend = joanna // (1)
await ClientStore.shared.addClient(john)
await ClientStore.shared.addClient(joanna) // (2)

 

  • (1)에서 john.fried = joanna를 수행하면, jonna는 이제 john을 통해 참조 (체이닝 property 접근)
  • 따라서, john과 jonna는 같은 ioslation region에 속함
  • (2)에서 joanna를 사용하는 코드가, ClientStore.shared 안에서 john.friend를 접근하는 코드와 동시에 실행될 수 있음 → 잠재적 데이터 경쟁 발생
  • 즉, 이미 다른 값과 연결되어 같은 region에 속한 non-Sendable 값은 동시 접근에 취약

 

 


출처

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md

 

swift-evolution/proposals/0414-region-based-isolation.md at main · swiftlang/swift-evolution

This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

github.com

 

반응형