Swift Concurrency 공식문서

YeoSungeun·2024년 10월 11일

동시성

비동기:

병렬: 동시에 여러부분이 실행됨

  • 동기 vs 비동기
    • "시간 흐름/기다림" 관점: 하나의 작업 단위 처리시 “기다리는가”
  • 직렬 vs 동시(하나일 수도) vs 병렬(멀티코어)
    • "처리 구조/실행 방식" 관점: 여러 작업이 있을 때 “어떻게 처리되는가”
  • 동시성 이라는 용어를 사용하여 비동기와 병렬 코드의 일반적인 조합을 나타냄

비동기 함수 정의와 호출

비동기함수

  • ”완료될 때까지 실행되거나/ 오류가 발생하거나/ 반환되지않거나”하는 실행 도중에 일시적으로 중단될 수 있는 특수한 함수
  • 파라미터 뒤에, → 전에, throws전에 async 키워드 작성
func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}
  • 비동기함수 호출시 중단 가능성 지점을 await으로 표시
  • Swift 가 await을 만나면 현재 쓰레드에서 코드의 실행을 일시 중단하고 대신 해당 쓰레드에서 다른 코드를 실행하기 때문에 이것을 쓰레드 양보 (yielding the thread) 라고함
  • 실행을 일시 중단할 수 있어야 하므로 프로그램의 특정 위치에서만 비동기 함수나 메서드를 호출할 수 있음
    • 비동기 함수, 메서드 또는 프로퍼티의 본문에 있는 코드
    • @main 으로 표시된 구조체, 클래스, 또는 열거형의 정적 (static) main() 메서드에 있는 코드
    • 구조화되지 않은 하위 작업의 코드
      • Task.yeilde() : 명시적으로 중단 지점 추가
      • Task.sleep(for:tolerance:clock:): 주어진 시간만큼 현재 작업 중단
        func listPhotos(inGallery name: String) async throws -> [String] {
            try await Task.sleep(for: .seconds(2))
            return ["IMG001", "IMG99", "IMG0404"]
        }

비동기 함수와 던지는 함수의 비교

  • 공통점
    • 표현방식 유사(async/await, throws/try)
    • 비동기 함수는 다른 비동기 함수를, 던지는 함수는 다른 던지는 함수를 내부에서 호출할 수 있음
  • 차이점
    • 던지는 함수는 Result 타입이나 do-catch 블록을 이용해 동기 코드에서도 호출 가능
    • 비동기 함수는 반드시 await로 호출해야 하며, 동기 코드에서는 호출 불가능(구현시 하면 race condition, 데드락 등 문제 발생)
  • throws는 동기 코드에서도 유연하게 처리 가능 (Result, do-catch)
  • async는 무조건 비동기 흐름 안에서만 실행 가능
  • 비동기 도입은 코드 구조를 재설계해야 하는 일, 작은 단위부터 위로가 아닌 최상위부터 아래로(top-down) 진행해야 함

비동기 시퀀스(Asynchronous Sequences)

  • for-await-in 루프는 다음 요소를 사용할 수 있을 때까지 기다리고 각 반복이 시작될 때 잠재적으로 실행을 일시 중단함
    import Foundation
    
    let handle = FileHandle.standardInput
    for try await line in handle.bytes.lines {
        print(line)
    }

비동기 함수 병렬로 호출 (Calling Asynchronous Functions in Parallel)

  • 비동기 함수를 호출하고 주변의 코드와 병렬로 실행시 상수를 정의할 때 let 앞에 async 를 작성하고 상수를 사용할 때마다 await 를 작성함
    async let firstPhoto = downloadPhoto(named: photoNames[0])
    async let secondPhoto = downloadPhoto(named: photoNames[1])
    async let thirdPhoto = downloadPhoto(named: photoNames[2])
    
    let photos = await [firstPhoto, secondPhoto, thirdPhoto]
    show(photos)

Task/ Task Groups

  • 작업 (task): 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위
  • 작업은 자기 자신은 한 번에 하나의 일만 처리하지만, 여러 개의 작업을 동시에 만들면 Swift가 이를 자동으로 병렬 실행하도록 스케줄링함

async let vs TaskGroup

async let

  • 정해진 수의 작업을 동시에 실행할 때 유용
  • 컴파일러가 자동으로 하위 작업을 생성
  • 예: 몇 개의 이미지 다운로드를 병렬 처리

TaskGroup

  • 작업 수가 동적일 때 적합 (for문 등에서 반복적으로 작업을 추가할 때)
  • 우선순위, 취소, 결과 누적 등을 제어할 수 있음
  • 명시적으로 하위 작업을 추가하고 관리 가능

작업 취소 (Task Cancellation)

  • Swift Concurrency는 협동 취소 모델(cooperative cancellation model)을 사용함
  • 작업(task)이 스스로 “취소되었는지”를확인하고 대응하는 방식

취소의 응답방식

  • CancellationError를 던짐 (에러 전파)
  • nil이나 빈 배열 반환 (부분적인 결과)
  • 완료된 작업만 반환 (진행 중이던 건 무시)

취소여부 확인 방법

  • checkCancellation()은 취소 시 에러로 처리 (간단)
  • isCancelled조건문으로 유연하게 대응 가능 (예: 정리, 로그, 반환값 조절 등)

취소 핸들러 사용(즉시반응 필요시)

let task = await Task.withTaskCancellationHandler {
    // ...
} onCancel: {
    print("Canceled!")
}

// ... some time later...
task.cancel()  // Prints "Canceled!"

구조화 되지 않은 동시성

  • 상위 작업과 관계 없이 동작해야 할 때
  • UI 바깥 또는 독립된 비동기 흐름이 필요할 때
  • 예: 알림 전송, 백그라운드 로깅, 앱 외부 이벤트 대응
  • Task {}를 사용해 현재 컨텍스트에서 독립적으로 작업 생성
  • Task.detached {}현재 actor나 context와 완전히 분리된 작업 생성

액터(Actors)

  • 동시성 환경에서 안전하게 상태를 보호하면서 데이터를 공유하기 위한 Swift의 구조
  • 클래스처럼 생겼지만, “한 번에 하나의 작업만 상태를 바꿀 수 있게 해주는 참조 타입”
  • 액터 외부 접근은 await 필요
    • 이유: 액터는 격리된 상태를 가지며, 외부 접근 시에는 비동기 중단점을 통해 조정해야 함

      let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
      print(await logger.max)  // ✅ OK
      print(logger.max)        // ❌ 컴파일 에러
      
  • 액터 내부 코드는 await 없이도 안전하게 실행
    • 중단 지점이 없기 때문에 실행 중간에 다른 코드가 끼어들 수 없음

전송 가능 타입 (Sendable Types)

  • Sendable은 Swift에서 동시성 도메인(concurrency domain) 간에 데이터를 안전하게 주고받을 수 있음을 나타내는 프로토콜
    • 작업(Task)이나 액터(Actor) 내부는 서로 다른 동시성 환경
    • 어떤 데이터를 이들 사이에서 주고받을 때, 해당 타입이 안전한지 보장해야함
    • Sendable을 만족하는 타입만이 동시성 도메인 간 전송 가능함

필요성

  • Swift에서는 여러 작업/스레드가 동시에 데이터를 읽고 쓸 수 있음
  • 만약 상태 보호 없이 공유하면 race condition, memory corruption, 예측 불가능한 버그 발생 가능
  • 따라서 안전한 타입만 공유할 수 있도록 명시적으로 제한→ Sendable의 역할

0개의 댓글