Swift - 18. 동시성 Concurrency

지우개·2022년 4월 5일
0

Swift study

목록 보기
7/15
post-thumbnail

Swift는 구조화된 방식으로 비동기(asynchronous)와 병렬(parallel) 코드 작성을 지원한다.

  • 비동기 코드는 일시적으로 중단되었다가 다시 실행할 수 있지만, 한 번에 프로그램의 한 부분만 실행된다. 짧은 작업을 계속 진행하면서 긴 실행 작업을 계속한다.
  • 병렬 코드는 동시에 코드의 여러 부분이 실행되는 것이다.

병렬 또는 비동기 코드는 추가 스케줄링의 유연성도 있지만 한편 복잡성이 증가하기도 한다.

비동기 함수/메소드 정의

  • 실행 도중에 일시적으로 중단될 수 있는 특수한 함수/메소드
  • 바디 내에서 실행을 일시중지할 수 있는 부분을 표시함
  • 파라미터 뒤, 반환 화살표 전에 async 키워드
  • throwing function이자 비동기 함수/메소드는 throws 전에 async를 작성
func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

호출

  • 비동기 메소드를 호출할 때 해당 메소드가 반환될 때까지 실행이 일시 중단됨
  • 중단될 가능성이 있는 지점을, 호출 앞에 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:)downloadPhoto(named:) 함수 모두 네트워크 요청을 필요로 하기 때문에 완료하는데 비교적 오랜시간이 걸릴 수 있다.
반환 화살표 전에 async 를 작성하여 둘 다 비동기로 만들면 이 코드는 그림이 준비될 때까지 기다리는 동안 앱의 나머지 코드가 계속 실행될 수 있다.

위 예제의 동시성을 이해하기 위한 실행 순서
1. 실행을 시작 ~ 첫번째 await 까지 실행된다. listPhotos(inGallery:) 함수를 호출하고 반환될 때까지 실행을 일시 중단한다.
2. 실행이 일시 중단되는 동안 같은 프로그램의 다른 코드가 동시에 실행된다. (예를 들어 오랜 시간 실행되어야 하는 백그라운드 작업인 새 사진의 목록 업데이트 등을 할 수 있다.) 이 코드는 await로 표시된 다음 중단 지점 또는 완료될 때까지 실행된다.
3. listPhotos(inGallery:)가 반환된 후에 이 코드는 해당 지점에서 시작하여 계속 실행된다. 반환된 값을 photoNames에 할당한다.
4. sortedNamesname을 정의하는 라인은 일반적인 동기 코드이다.
5. 다음 awaitdownloadPhoto(named:) 함수에 대한 호출을 표시한다.
6. downloadPhoto(named:)가 반환된 후에 반환값은 photo에 할당된 다음에 show(_:)를 호출할 때 인수로 전달된다.

Swift가 현재 스레드에서 코드의 실행을 일시 중단하고 대신 해당 스레드에서 다른 코드를 실행하기 때문에 스레드 양보(yielding the thread)라고도 부른다.
await가 있는 코드는 실행을 일시 중단할 수 있어야 하므로 프로그램의 특정 위치에서만 비동기 함수/메소드를 호출할 수 있다.

  • 비동기 함수, 메소드 또는 프로퍼티의 바디에 있는 코드
  • @main으로 표시된 구조체, 클래스, 또는 열거형의 정적 (static) main() 메소드에 있는 코드
  • 구조화되지 않은 하위 작업의 코드

Task.sleep(_:) 메소드는 동시성 작동 방식을 위해 간단한 코드를 작성할 때 유용하다. 이 메소드는 아무런 동작도 하지 않지만 반환되기 전에 주어진 나노 단위의 초만큼 기다린다. 다음은 네트워크 작업 대기를 시뮬레이션하기 위해 sleep(nanoseconds:)을 사용하는 listPhoto(inGallery:) 함수의 버전이다:

func listPhotos(inGallery name: String) async throws -> [String] {
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000)  // Two seconds
    return ["IMG001", "IMG99", "IMG0404"]
}

비동기 시퀀스 Asynchronous Sequences

이전 섹션에서는 비동기적으로 배열의 모든 요소가 준비된 후에 전체 배열을 한번에 반환했다.
또 다른 접근 방식은 비동기 시퀀스(asynchoronous sequence)를 사용하여 한번에 콜렉션의 한 요소를 기다리는 것이다.

import Foundation

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

for - await - in 루프는 다음 요소를 사용할 수 있을 때까지 기다리고 각 반복이 시작될 때 잠재적으로 실행을 일시 중단한다.


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

await을 사용하여 비동기 함수를 호출하면 한번에 코드의 한 부분만 실행된다.
비동기 코드가 실행되는 동안 호출자는 코드의 다음 라인을 실행하기 위해 이동하기 전에 해당 코드가 완료될 때까지 기다린다.

예를 들어 다음 예제에서는 downloadPhoto(named:) 함수에 대해 세 번의 호출을 기다린다.

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)

이 방식으로 할 경우, 다운로드가 비동기이고 진행되는 동안 다른 작업을 수행할 수 있지만 downloadPhoto(named:)에 대한 호출은 한번에 하나만 실행되는 단점이 있다.

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

이 방식에서는 downloadPhoto(named:)을 호출하는 세 가지는 모두 이전 호출이 완료되길 기다리지 않고 시작된다. 코드가 함수의 결과를 기다리기 위해 일시 중단되지 않기 때문에 await로 표시하지 않고, photos가 정의된 라인까지 계속 실행된다.

두 접근의 차이점:

  • 다음 줄의 코드가 해당 함수의 결과에 따라 달라지면 await를 사용하여 비동기 함수를 호출한다. 이는 순차적으로 실행되는 작업을 생성한다.
  • 나중에 코드에서 결과가 필요하지 않을 때 async-let을 사용하여 비동기 함수를 호출한다. 이렇게 하면 병렬로 수행할 수 있는 작업이 생성된다.
  • awaitasync-let은 모두 일시 중단되는 동안 다른 코드를 실행할 수 있도록 한다.
  • 두 경우 모두 비동기 함수가 반환될 때까지 필요한 경우 실행이 일시 중단됨을 나타내기 위해 가능한 일시 중단 지점을 await로 표시한다.

작업과 작업 그룹 Tasks and Task Groups

작업이란?

  • 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위
  • 계층 구조로 정렬됨
  • 구조적 동시성 structured concurrency
  • 작업 간의 명시적 부모-자식 관계를 통해 취소 전파(propagating cancellation)와 같은 일부 동작을 처리할 수 있음

구조화되지 않은 동시성 Unstructured Concurrency

구조화되지 않은 작업

  • 상위 작업이 없다.
  • 프로그램이 필요한 방식으로 관리할 수 있다. - 완전한 유연성
  • 완전한 정확성을 갖춰야 한다.
  • 현재 행위자(actor)에서 구.않.작. 생성: Task.init(priority:operation:) 초기화 구문 호출
  • 현재 행위자의 일부가 아닌 구.않.작. 생성: Task.detached(priority:operation:) 클래스 메소드 호출
  • 작업과 상호작용할 수 있는 작업 핸들(task handle)을 반환한다. -> 결과를 기다리거나 취소
let newPhoto = // ... some photo data ...
let handle = Task {
    return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

작업 취소 Task Cancellation

Swift 동시성은 협동 취소 모델(cooperative cancellation model)을 사용한다.

  • CancellationErro와 같은 에러 발생
  • nil 또는 빈 콜렉션 반환
  • 부분적으로 완료된 작업 반환

취소를 확인

  • Task.checkCancellation()을 호출하거나
  • Task.isCancelled의 값을 확인하고 자체 코드에서 취소를 처리함

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


행위자 Actors

  • 참조 타입
  • (클래스와 다르게) 한 번에 하나의 작업만 변경 가능한 상태에 접근하도록 허용하므로
  • 여러 작업의 코드가 행위자의 동일한 인스턴스와 상호작용하는 것은 안전하다

온도를 기록하는 행위자 예제

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
    }
}

0개의 댓글