이 포스팅은 Swift 시리즈 20 편 중 20 번째 글 입니다.

  • Part 1 - 01: Optional, Any, AnyObject, nil
  • Part 2 - 02: struct, class, enum
  • Part 3 - 03: Closure
  • Part 4 - 04: Property
  • Part 5 - 05: 상속, 생성자, 소멸자
  • Part 6 - 06: 옵셔널 체이닝과 nil 병합
  • Part 7 - 07: 타입 캐스팅
  • Part 8 - 08: assert, guard
  • Part 9 - 09: Protocol
  • Part 10 - 10: Extention
  • Part 11 - 11: 오류처리
  • Part 12 - 12: 고차함수
  • Part 13 - 13: ARC(Automatic Reference Counting)
  • Part 14 - 14: Access control, Access modifier
  • Part 15 - 15: Generics
  • Part 16 - 16: Optional에 대한 깊은 이해
  • Part 17 - 17: Lazy Variables
  • Part 18 - 18: Enumeration
  • Part 19 - 19: Initialization
  • Part 20 - This Post
▼ 목록 보기

동시성

  • 코어의 개수만듬 코드를 실행시킬 수 있다.
  • 4코어면 4개의 코드를 병렬적으로 실행가능

Async, Await

callback 함수 (Completion Handler)를 사용하면 코드의 읽기가 상당히 어려워짐

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

함수 자체의 응답이 비동기로 돌아가는 경우, 비동기 함수임을 명시할 수 있음

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

만약 에러까지 던지는 경우 아래와 같이 쓰면 된다.

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

이렇게 5.5버전 부터 async, await를 사용하면 아래와 같이 간결하게 표현 가능

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
  • 실행 순서
    • await를 본다.
    • 함수를 호출한다.
    • 그리고 해당 코드의 실행을 대기한다. (suspend)
    • 현재 작업을 진행하는 스레드는 멈춰 있는 것이 아니고, 다른 동시성 코드를 실행시킨다.
      • 예를 들어 포토 갤러리 리스트를 업데이트 하는 것 같은.
    • 해당 작업이 끝나면 할당된다.
    • 다시 await 작업을 수행한다.
    • 반환 받고 보여준다.
  • 이렇게 스레드를 일시정지 하고(await) 기다리는 행위를 yieling 이라고 함

비동기 시퀀스

import Foundation

let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
    print(line)
}
  • 위의 listPhotos 함수의 경우 비동기로 모든 요소를 다 만든다음에 리턴을 하게 된다.
  • 그런데 비동기로 한 요소를 받았을 때마다 보내줄 수 도 있다.
  • 위와 같이 AsyncSequence 프로토콜을 사용한 녀석을 사용하면 하나씩 보낼 수 있다.

병렬로 비동기 함수 호출하기

  • 여러 비동기 함수를 호출한다음에 전체 결과를 만들고 반환하려면 이와 같이 쓸 수 있다.
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)
  • 그런데 이렇게 되면 작업을 하나씩 (왜냐면 일시정지되니까) 하게되서 비효율적이다.
  • 그래서 아래와 같은 방법을 사용할 수 있다.
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)
  • 각각의 변수가 미래에 받아진다고 선언해두고, 다 받아진 경우 awiat를 가지고 확인한뒤 리턴한다.

작업과 작업 그룹

  • 비동기 작업을 묶어서 관리가능
  • 그리고 그 작업들의 우선 순위를 만들 수 있음
  • 취소도 가능
  • 계층 구조로 관리된다.
  • 그래서 작업 그룹안에 작업들은 공통의 부모 작업을 가지게 되고
  • 각각의 작업은 자식 작업을 가질 수 있다.
  • 이렇게 구조적으로 작업을 명시할 수 있기 때문에 이를 structed Concurrency라 부른다.
  • 당연히 이렇게 구조화된 작업을 잘 만드는 책임은 개발자에게 있다.
await withTaskGroup(of: Data.self) { taskGroup in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        taskGroup.addTask { await downloadPhoto(named: name) }
    }
}
  • 하나의 큰 테스크 그룹을 만들고, 그 안에 다른 비동기 작업들을 걸어둘 수 있다.
  • 이렇게 되면 내부에 있는 taskGroup 안에 다른 task 들이 들어가게 된다.
  • 작업의 취소는 canceled 를 사용하여 처리하고 isCanceled 함수를 통해 확인할 수 있다.

Actor

  • 참조형
  • 그런데, 그러면 비동기로 작업할 때, 그 뭐냐..그… 동시성 이슈가 발생할 수 있음
  • 그런데 actor의 경우 여러 task의 코드가 같은 인스턴스에 상호작용하는 것을 안전하게 해줌
  • 스레드 안전하다는 것
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
    }
}

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

  • 클래스 선언하는 것과 비슷하다.
let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
print(await logger.max)
// Prints "25"

만약에 이렇게 만들었는데, 다른 객체에서 TemperastureLogger에 접근해서 작업을 수행하고 있다면, 이 코드는 당장 접근이 불가하고, 해당 작업이 끝난 이후에 접근해서 값을 가져올 수 있다.

만약에 이렇게 스레드 안전하게 안만들어두면 이와 같은 상황이 발생가능하다.

  1. update 메서드 호출함
  2. measurements 어레이를 업데이트함
  3. 다른 스레드에서 max가 업데이트 되기전에 max 값을 읽음
  4. max를 업데이트함

이런 행위를 actor isolation 이라 함

Reference