GGURUPiOS

Swift 공식 문서 정리 - (18) Concurrency 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (18) Concurrency

꾸럽 2023. 5. 14. 15:35

Concurrency ( 동시성 )

스위프트는 구조화된 방식으로 비동기 및 병렬 코드 작성을 지원 함

비동기 코드는 일시 중지 했다가 나중에 재개할 수 있음

네트워크를 통해 데이터를 가져오거나 파일 구문 분석과 같은 장기 실행 작업을 하면서

UI 업데이트와 같은 단기 작업을 수행할 수 있음

병렬 또는 비동기 코드의 스케줄링 유연성은 복잡성 증가 비용과 함께 적용됨

스위프트의 언어지원을 사용하지않고 동시성 코드를 작성할 수 있지만 읽기 더 어려운 경향이있음

listPhotos(inGallery: "Summer Vacation") { photoNames in
    let sortedNames = photoNames.sorted()
    let name = sortedNames[0]
    downloadPhoto(named: name) { photo in
        show(photo)
    }
}

이 간단한 경우에도 코드가 일련의 완료 핸들러로 작성되어야 하므로 중첩된 클로저를 작성하게 됨

→ 다루기 어려워 질 수 있음

비동기 함수 정의 및 호출

비동기 함수 또는 비동기 메서드는 실행 도중 일시 중지할 수 있는 특수한 종류의 함수 또는 메서드임

이는 완료될 때까지 실행되거나 오류가 발생하거나 반환되지 않는 일반적인 동기식 함수 및 메서드와 대조 됨

함수 또는 메서드가 비동기임을 나타내려면 throwing 함수를 표시하는 데 사용하는 방법과 유사하게 async 키워드를 작성함

아래와 같이 작성한다.

func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

비동기 메서드를 호출하면 해당 메서드가 반환될 때 까지 실행이 일시 중단 됨.

가능한 연기 지점에 await 키워드를 작성할 수 있음.

try 키워드를 적는 것 처럼 작성하면 된다. 비동기 메서드 내에서 실행 흐름은

다른 비동기 메서드를 호출할 때만 일시 중단 됨

일시 중단은 암시적이거나 선점적이지 않고

가능한 모든 일시 중단 지점이 await로 표시될 수 있음

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)

listPhotos(inGallery:) 함수와 donloadPhoto(named:) 함수 둘다 네트워크 요청을 필요로 하기 때문에, 긴 시간이 걸릴 수 있음

둘다 함수 모드 반환 화살표 이전에 async 를 선언하였기 때문에, 기다리는 동안 나머지 앱의 코드들이 실행 됨

위 예제의 동시 특성을 이해하기 위해 가능한 실행 순서는 다음과 같음

  1. 코드는 첫줄 부터 실행을 시작하여 첫 await 까지 실행 됨. listPhoto 함수를 부르고, 완료될 때 까지 실행을 연기 함.
  2. 실행이 연기된 동안, 다른 동기코드들이 실행됨. 예를들어, 장기 실행 백그라운드 작업이 새 사진 갤러리 목록을 계속 업데이트 할 수 있음. 그 코드들은 다음 await 까지 실행 되거나 listPhoto 함수가 완료될 때 까지 실행됨
  3. listPhoto가 반환된 후에 해당 시점부터 계속 실행한다. photoNames에 값 할당
  4. sortedNames 는 동시성 코드이다. await 키워드가 없으므로, 가능한 연기 지점이 없다.
  5. 다음 await에서 downloadPhoto(named:) 함수를 호출함. 이 코드도 역시 반환때까지 정지됨.
  6. downloadPhoto 를 받은후에, photo 에 할당함. 그다음 show에 인자를 넘겨줌

await로 표시된 일시 정지 지점은 현재 코드 부분이 실행을 일시 정지할 수 있음을 나타냄

스위프트는 현재 스레드에서 코드 실행을 일시 중단하고 해당 스레드에서 다른 코드를 실행하기 때문에 yielding the thread ( 스레드 양보? 정도로 해석 ) 이라고 할 수 도 있음.

await 코드는 실행을 일시 정지 할 수 있어야 하므로 프로그램의 특정 위치에서만 비동기 함수를 호출 할 수 있음

특정 위치?

  • 비동기 함수, 메서도 또는 프로퍼티의 본문에 있는 코드
  • @main으로 표시된 구조체, 클래스 또는 열거형의 정적 main() 메서드의 코드입니다.
  • 아래의 비정형 동시성에 표시된 것처럼 비정형 하위 작업의 코드입니다.

가능한 정지 지점 사이의 코드는 다른 동시 코드의 중단 가능성 없이 순차적으로 실행 됨

let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
add(firstPhoto, toGallery: "Road Trip")
// At this point, firstPhoto is temporarily in both galleries.
remove(firstPhoto, fromGallery: "Summer Vacation")

add 함수와 Remove 함수 사이에는 다른 코드를 실행할 방법이 없다. 첫번째 사진이 양쪽 갤러리 모두에서 나타나 일시적으로 앱의 불변성 중 하나를 깨트린다.

명확하게 하기위해 await를 사용해서 동기 함수로 리팩토링 할 수 있음.

func move(_ photoName: String, from source: String, to destination: String) {
    add(photoName, toGallery: destination)
    remove(photoName, fromGallery: source)
}
// ...
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")

위의 예에서 함수는 동기식이므로 일시 중단 지점을 포함할 수 없음을 보장함. 앞으로 이 함수에 동기 코드를 추가하려고 하면 일시 중단 지점이 생길 수 있으므로 컴파일 오류가 발생함

비동기 시퀀스

이전 섹션의 함수는 배열의 모든 요소가 준비된 후 전체 배열을 한번에 비동기 식으로 반환함

또 다른 방법은 비동기 시퀀스를 사용해서 한 번에 컬렉션의 한 요소를 기다리는 것.

import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}

/*
루프뒤에 await 를 써서 일시 중단 지점을 나타냄.
for - await - in 루프는 다음 요소를 사용할 때 까지 기다리는 각 반복의 시작점에서 잠재적으로 실행을 일시 중단
*/

병렬로 비동기 함수 호출

await를 사용하여 비동기 함수를 호출하면 한 번에 하나의 코드만 실행 됨

비동기 코드가 실행되는 동안 호출자는 다음 코드 줄을 실행하기 위해 이동하기 전에 해당 코드가 완료될 때 까지 기다림

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

//갤러리에서 처음 세장의 사진을 가져오려는 함수에 대한 세번의 호출을 기다림

이 접근 방식에는 중요한 단점이 있다. 다운로드가 비동기식이고 진행되는 동안 다른 작업이 발생하도록 허용하지만 한 번에 하나의 호출만 실행 됨.

1번째 사진완료 → 2번째 사진완료 → 3번째 사진완료 이런식으로 진행됨

그러나, 각 사진을 개별적 또는 동시에 다운로드 할 수 있음

비동기 함수를 호출하고 주변의 코드와 병렬로 실행되도록 하려면 상수를 정의할 때 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)

위의 예에서, 세개의 downloadPhoto(named:)함수는 기다림 없이 실행된다 ( 병렬 )

함수의 결과를 기다리는 동안 코드가 await로 일시 중단 되지않기 때문에, 함수 호출은 대기로 표시되지 않음

대신에, photos가 정의된 줄까지 실행이 계속됨. 이 시점에서 프로그램은 비동기 호출의 결과를 필요로 하므로 세 사진 모두 다운로드가 완료될 때까지 실행을 일시 중지하기 위해 대기를 사용

이 두 접근 방식의 차이점

  • 다음 줄의 코드가 해당 함수의 결과에 따라 달라지면 await 상태로 비동기 함수를 호출함, 동기작업이 생성됨
  • 코드의 나중까지 결과가 필요하지 않을 때, 비동기식 함수를 async-let 으로 호출 함. 병렬로 수행할 수 있는 작업을 만듬.
  • await, async-let 모두 그들이 일시 중지된동안 다른 코드를 실행할 수 있음
  • 두 경우 모두 가능한 일시 중단 지점에 await 표시를 하여 필요한 경우 비동기 기능이 돌아올 때까지 실행이 일시 중지됨을 나타냄.

또한 이 두 가지 접근 방식을 동일한 코드로 혼합할 수 있음

작업 및 작업그룹 (TaskGroup)

작업은 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위임.

모든 비동기 코드는 일부 작업의 일부로 실행 됨

이전에 설명한 async-let 문법은 하위 작업을 만듬

또한 작업그룹을 만들수 있고 해당 그룹에 하위 작업을 추가할 수도 있음.

작업은 계층 구조로 정렬됨. 작업 그룹의 각 작업에는 동일한 상위 작업이 있으며 각 작업에는 하위 작업이 있을 수 있음

작업과 작업 그룹간의 명시적인 관계로 인해 이 접근 방식을 구조적(구조화된) 동시성 이라고 함

정확성에 대한 일부 책임은 사용자에게 있지만 작업 간의 명시적인 부모-자식 관계를 통해

swift는 최소 전파와 같은 일부 동작을 처리하고 swift는 컴파일 시간에 일부 오류를 감지할 수 있음

await withTaskGroup(of: Data.self) { taskGroup in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        taskGroup.addTask { await downloadPhoto(named: name) }
    }
}

구조화되지 않은 동시성 (Task)

구조화되지 않은 동시성 또한 지원함

작업 그룹의 일부인 작업과 달리 구조화되지 않은 작업에는 상위 작업이 없음

현재 액터에서 실행되는 구조화되지 않은 작업을 만들려면 초기화 프로그램 (Task.init(priority:operation:)) 을 호출함

현재 액터의 일부가 아닌 구조화되지 않은 작업을 만들려면 클래스 메서드 호출(Task.detached(priority:operation:)) 호출

액터? → 아래에서 자세히 설명함.

이 두 작업 모두 결과를 기다리거나 취소하는 등 상호 작용할 수 있는 작업을 반환 함

let newPhoto = // ... some photo data ...
let handle = Task {
    return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

작업 취소

스위프트 동시성은 협력 취소 모델을 사용함

각 작업은 실행 중 적절한 시점에 취소된 여부를 확인하고 적절한 방식으로 취소에 응답

수행중인 작업에 따라 일반적으로 다음 중 하나를 의미함

  • CancelllationError 와 같은 에러를 던진다
  • nil을 반환하거나 빈 컬렉션을 반환한다
  • 부분적으로 완료된 작업을 반환한다

취소를 확인하려면 Task.checkCancelation() 를 호출하여 작업이 취소된 경우 CancelationError를 발생시키거나 Task.is Cancelled 값을 확인하고 자신의 코드로 취소를 처리 함

예를 들어 갤러리에서 사진을 다운로드하는 작업에서는 부분 다운로드를 삭제하고 네트워크 연결을 닫아야 할 수 있음

취소를 수동으로 전파하려면 Task.cancel()을 호출 함

액터 (Actor)

작업을 사용하여 프로그램을 격리된 동시 조각으로 나눌 수 있음

작업은 서로 격리되어있어 동시 실행하는 것이 안전하지만 작업 간에 일부 정보를 공유해야 하는 경우가 있음

액터를 사용하면 동시 코드 간에 정보를 안전하게 공유 가능

액터는 참조유형이고, 클래스와 달리 액터는 한 번에 하나의 작업만 변경 가능한 상태에서 액세스 할 수 있도록 허용 하므로 여러 작업의 코드가 동일한 액터 인스턴스와 상호 작용하는것이 안전함

→ 무슨말인지 잘 모르겠음. 예제로 알아보자

온도를 기록하는 액터

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int

    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
}

actor 키워드를 사용하여 actor를 명명하고, 정의를 대괄호로 표시

TempartureLogger 액터에는 액터 외부의 다른코드가 액세스 할 수 있는 속성이 있으며 (label, measurements) 최대 속성을 제한하여 액터 내부의 코드 만 최대값을 업데이트 할 수 있음(private(set))

구조체, 클래스와 동일한 이니셜라이저 구문을 사용하여 인스턴스를 만듬. 액터의 프로퍼티나 메서드에 액세스할 때 await를 사용하여 잠재적인 일시 중단 지점을 표시함

예를들어

let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"

이 예시 에서는 logger.max에 액세스하는것이 가능한 중단 지점임. 액터는 한 번에 한 작업만 가변 상태에 액세스할 수 있도록 허용하므로 다른 작업의 코드가 이미 로거와 상호 작용하고 있으면

이 코드는 속성에 액세스하기 위해 대기하는 동안 일시 중단 됨.

이와 대조적으로, 액터의 일부로 작성된 코드는 액터의 프로퍼티에 액세스할 때 await를 쓰지 않는다. 예를들어 Temparature Logger를 새 온도로 업데이트 하는 방법은 다음과 같음

extension TemperatureLogger {
    func update(with measurement: Int) {
        measurements.append(measurement)
        if measurement > max {
            max = measurement
        }
    }
}

update메소드가 이미 액터의 내부의 코드 이므로 max와 같은 프로퍼티에 접근할 때 await를 쓰지 않는다. 이 메소드는 한 번에 하나의 작업만 그들의 가변 상태와 상호 작용하도록 허용하는 이유중 하나를 보여줌

액터상태에 대한 일부 업데이트는 일시적으로 불변성을 깨뜨림

Temperature Logger(온도 기록기) 액터는 온도 목록과 최대 온도를 추적하며 새 측정값을 기록할 때 최대 온도를 업데이트 함

업데이트 도중에 새 측정 값을 추가한 후 최대 값을 업데이트하기 전에 온도 로거가 일시적으로 일정하지 않은 상태가 됨

여러 작업이 동일한 인스턴스와 동시에 상호작용하지 않도록 하면 다음과 같은 일련의 이벤트와 같은 문제가 방지 됨

  • 코드에서 update(with:) 메서드를 호출함. 먼저 measurement 배열을 업데이트함
  • 코드를 업데이트 하기 전에 다른 곳의 코드에서 최대값과 온도 배열을 읽음
  • 코드는 max값을 변경하여 업데이트를 완료 함

update(with:) 이 경우 데이터가 일시적으로 유효하지 않은 동안 액터에 대한 액세스가 호출 중간에 이터리브되었기 때문에 다른 곳에서 실행되는 코드는 잘못된 정보를 읽음.

중단 지점이 없기 때문에 update(with:) 업데이트 중에 다른 코드가 데이터에 액세스할 수 없음

클래스의 인스턴스에서와 같이 액터 외부에서 이러한 속성에 액세스 하려고 하면 컴파일 오류가 발생 함

print(logger.max) // Error

액터의 logger.max 속성이 해당 액터의 격리된 로컬 상태의 일부이기 때문에 await 없이 액세스는 실패함.

스위프트는 액터 내부의 코드만 액터의 로컬 상태에 액세스 할 수 있음을 보장함

이 보장을 액터 격리라고 함

정리

→ 좀 말이 어려운데 내가 이해한 바는 아래와 같다

  1. 액터는 작업 간에 일부 정보를 공유해야 하는 경우 Task, TaskGroup 대신 쓰기에 적당함
  2. 액터 내부에 접근하려면 await를 사용해서 접근 가능함
  3. 액터 내부에서 내부의 값에 접근하면 await 없이 사용 가능함

보낼 수 있는 타입 (Sendable Types)

태스크와 액터를 사용하면 프로그램을 안전하게 실행할 수 있는 조각으로 나눌 수 있음

태스크와 액터의 인스턴스 내부에서 변수 및 프로퍼티와 같은 가변 상태를 포함하는 프로그램 부분을 동시 도메인이라고 함.

데이터에 가변 상태가 포함되어 있지만 중복 액세스로부터 보호되지 않기 때문에 일부 데이터 유형은 동시성 도메인 간에 공유할 수 없음.

한 동시성 도메인에서 다른 도메인으로 공유할 수 있는 유형을 전송 가능 타입이라고 함

예를들어, 액터 메서드를 호출할 때 인자로 전달되거나 태스크의 결과로 반환 될 수 있음

이 앞부분의 예제에서는 항상 안전하게 공유할 수 있는 단순 값을 사용하기 때문에 전송 가능성에 대해 설명하지 않음

반대로 일부 타입은 동시성 도메인을 통과하는 것이 안전하지 않음.

예를들어, 가변 속성을 포함하고 해당 프로퍼티에 대한 액세스를 직렬화 하지 않는 클래스는 해당 클래스의 인스턴스를 다른 작업 간에 전달할 때 예측할 수 없고 잘못된 결과를 생성할 수 있음

전송 가능 프로토콜을 선언하여 타입을 전송 가능으로 표시함.

이 프로토콜에는 코드 요구 사항이 없지만 의미론적 요구 사항이 있음

3가지 요구사항

  • 타입은 값 타입이며, 가변 상태는 다른 전송 가능한 데이터(예: 전송 가능한 프로퍼티가 저장된 구조체 또는 열거형)으로 구성됨
  • 타입에는 변경 가능한 상태가 없으며, 변경 불가능한 상태는 다른 전송 가능한 데이터(예: 읽기 전용 속성만 있는 구조체 또는 클래스) 로 구성됨
  • 타입에는 @MainActor로 표시된 클래스나 특정 스레드 또는 대기열에서 프로퍼티에 대한 액세스를 직렬화하는 클래스와 같이 가변 상태의 안전을 보장하는 코드가 있음

전송 가능한 속성만 있는 구조체와 전송 가능한 관련 값만 있는 열거형처럼, 일부 타입은 항상 전송 가능함

struct TemperatureReading: Sendable {
    var measurement: Int
}

extension TemperatureLogger {
    func addReading(from reading: TemperatureReading) {
        measurements.append(reading.measurement)
    }
}

let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading)

TemperatureReading은 전송 가능한 속성만 있는 구조이며 이 구조는 public 또는 @usableFromInline으로 표시되지 않으므로 암시적으로 전송 가능.

다음은 전송 가능 프로토콜에 대한 준수를 암시하는 구조의 버전임:

struct TemperatureReading {
    var measurement: Int
}