Swift 5.5 Release(2)

유재경·2021년 6월 11일

iOS New Release

목록 보기
2/12
post-thumbnail

(※ hackingWithSwift의 글을 번역한 것으로 아래 출처를 남겨두었습니다. 약간의 오역이 있을 수 있으니 지적해주시면 감사드리겠습니다.)

Structured Concurrency

직역하자면 구조화된 동시성이다. async/await와 async시퀀스 작업에 concurrent operation을 실행, 취소, 관찰할 수 있는 것을 도입했다고 한다.
예제로 살펴보는 것이 빠르겠다.

enum LocationError: Error {
    case unknown
}

func getWeatherReadings(for location: String) async throws -> [Double] {
    switch location {
    case "London":
        return (1...100).map { _ in Double.random(in: 6...26) }
    case "Rome":
        return (1...100).map { _ in Double.random(in: 10...32) }
    case "San Francisco":
        return (1...100).map { _ in Double.random(in: 12...20) }
    default:
        throw LocationError.unknown
    }
}

func fibonacci(of number: Int) -> Int {
    var first = 0
    var second = 1

    for _ in 0..<number {
        let previous = first
        first = second
        second = previous + first
    }

    return first
}

하나는 지역에 따라 특정 숫자를 반환하는 비동기 작업이고, 나머지는 익히 본 피보나치 수열에서 특정 숫자를 반환하는 동기 작업이다.

이 때, @main 속성을 사용하고, 비동기 함수 앞에 main() 메소드를 함께 사용하면 곧장 비동기 맥락,환경(async context)로 가게끔 할 수 있다는 것이 structured concurrency에 의해 도입된 가장 심플한 것이다.

@main
struct Main {
    static func main() async throws {
        let readings = try await getWeatherReadings(for: "London")
        print("Readings are: \(readings)")
    }
}

Task, TaskGroup

structured concurrency의 가장 주요한 변화라고 한다. 동시성 연산을 각각 혹은 조합해서 사용할 수 있게 한다고 하는데..

단순하게 표현하자면 새로운 Task 객체를 생성하여 동시성 작업(concurrent work)을 시작할 수 있고 이것을 원하는 연산에 전달해줄 수 있다는 것이다. 그러면 이 작업은 백그라운드 스레드에서 돌기 시작할 것이고, 우리는 최종 값이 리턴될 때까지 await를 사용해서 기다릴 것이다.(즉, 다른 작업을 할 수 있으면서 값이 오는 것을 기다릴 수 있다.)

이것도 예제로 보는 것이 빠를 것 같다.

func printFibonacciSequence() async {
    let task1 = Task { () -> [Int] in
        var numbers = [Int]()

        for i in 0..<50 {
            let result = fibonacci(of: i)
            numbers.append(result)
        }

        return numbers
    }

    let result1 = await task1.value
    print("The first 50 numbers in the Fibonacci sequence are: \(result1)")
}

우리가 위 함수의 결과를 얻기 위해 백그라운드 스레드에서 함수를 재귀적으로 호출될 것을 압니다. Task { () -> [Int] in ... } (혹은 Task {...} 로 표현) 를 명시함으로써 swift는 작업이 리턴될 것을 인지하게 된다.

Tip: 참고로 class나 struct 내에서 Task를 쓸 때, 속성이나 메소드에 접근하기 위한 self를 명시하지 않아도 된다고 합니다. 왜냐하면, 작업은 나중을 위해 연산을 저장하는 것 이 아니라 바로 실행되는 non-escaping closure이기 때문입니당. 반면, escaping closure는 값을 저장해두거나 비동기 작업을 할 때 사용되며 self를 명시하라고 합니다. https://www.c-sharpcorner.com/article/what-is-escaping-and-non-escaping-closure-in-swift/ esacping closure 과 non-esacping closure에 대한 자료입니다.

func runMultipleCalculations() async throws {
    let task1 = Task {
        (0..<50).map(fibonacci)
    }

    let task2 = Task {
        try await getWeatherReadings(for: "Rome")
    }

    let result1 = await task1.value
    let result2 = try await task2.value
    print("The first 50 numbers in the Fibonacci sequence are: \(result1)")
    print("Rome weather readings are: \(result2)")
}

let result1 = await task1.value의 'value'는 Error를 자동으로 던질 수 있는 프로퍼티..!! 결론적으로 우리는 Task와 value 프로퍼티를 사용함으로써 두 작업을 동시에 수행하고 완료되기를 기다릴 수 있게 된다.

추가적으로, Task의 우선순위를 정할 수 있다고 한다! 종류로는 high, default, low, background가 있고 Task(priority:)에 넣어주면 된다. 명시하지 않으면 당연히 default로 설정! userInteractive는 메인 스레드를 위한 것으로 여기서는 불가능하다.

Task의 static method

코드가 실행되는 것을 통제할 수 있는 몇 가지 정적 함수도 존재하는데,
1. Task.sleep() : 말 그대로 현재 작업을 해당 시간동안 sleep 시킴
2. Task.checkCancellation() : 어디선가 cancel() 메소드를 호출해서 해당 작업을 취소시켰는지 확인하는 것이다. 만약 cancel()로 해당 작업을 호출하고자 했다면 CancellationError를 던지는 것이다.
3. Task.yield() : 기다리고 있을지 모르는 다른 작업을 위해 현재 작업이 잠시 양보, 중단하는 것이다. loop로 집약적인 작업 중일 때 중요하다

다음은 1,2번을 사용한 예시이다.

func cancelSleepingTask() async {
    let task = Task { () -> String in
        print("Starting")
        await Task.sleep(1_000_000_000)
        try Task.checkCancellation()
        return "Done"
    }

    // The task has started, but we'll cancel it while it sleeps
    task.cancel()

    do {
        let result = try await task.value
        print("Result: \(result)")
    } catch {
        print("Task was cancelled.")
    }
}

분명 중간에, task.cancel()이 호출되어 CancellationError를 즉시 던질 것이지만, task.value가 호출될 때 우리에게 CancellationError가 나타난다.

Tip: task.value가 아닌 task.result를 사용하면 작업의 성공/실패 여부를 포함한 Result 타입으로 얻을 수 있다.

좀 더 복잡한 작업의 경우, TaskGroup을 사용한다. 리스크를 줄이기 위해 public initializer를 사용하는 것이 아닌, withTaskGroup() 함수를 사용해서 task group을 만든다.
※주의: withTaskGroup() 밖에서 task group을 복사하는 행위는 절.대. 금지라고 한다.

예시를 살펴볼 시간입니당

func printMessage() async {
    let string = await withTaskGroup(of: String.self) { group -> String in
        group.async { "Hello" }
        group.async { "From" }
        group.async { "A" }
        group.async { "Task" }
        group.async { "Group" }

        var collected = [String]()

        for await value in group {
            collected.append(value)
        }

        return collected.joined(separator: " ")
    }

    print(string)
}

결과적으로 "Hello", "From", "A", "Task", "Group" 이 조합되어 나오지만 비동기적으로 처리되기 때문에 순서가 뒤집혀서 나올 가능성이 있다.

Tip: task group은 모두 같은 타입을 반환해야 하므로, enum을 사용하는 것을 추천한다. 좀 더 심플한 대안은 뒤에서 살펴볼 Async Let Binding이다.

에러를 핸들링에 관해,
1. task group 내에서 에러를 처리하는 법
2. task group 밖에서 에러를 처리하는 법 ->withThrowingTaskGroup() + try

func printAllWeatherReadings() async {
  do {
      print("Calculating average weather…")

      let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in
          group.async {
              try await getWeatherReadings(for: "London")
          }

          group.async {
              try await getWeatherReadings(for: "Rome")
          }

          group.async {
              try await getWeatherReadings(for: "San Francisco")
          }

          // Convert our array of arrays into a single array of doubles
          let allValues = try await group.reduce([], +)

          // Calculate the mean average of all our doubles
          let average = allValues.reduce(0, +) / Double(allValues.count)
          return "Overall average temperature is \(average)"
      }

      print("Done! \(result)")
  } catch {
      print("Error calculating data.")
  }
}

위 코드는 async()를 각각 호출했지만 location in ["London", "Rome", "San Francisco"] { to call async()... } 이런 식으로 쓰면 좀 더 간결하다.

TaskGroup의 메소드로,
cancelAll()은 모든 작업을 취소시킬 수 있지만, 이후에 async() 가 나타난다면 그룹에 작업을 추가할 것이다. 그 대안은asyncUnlessCancelled()를 사용하는 것으로 그룹이 cancel되면 더 이상 작업을 추가하지 않게 할 수 있다.


동시성을 가능하게 하는 Task에 대해 알아보았고 다음에는 async let binding과 동기 코드로 비동기 작업에 결합시키는 것에 대해 알아보도록 하자..

출처: https://www.hackingwithswift.com/articles/233/whats-new-in-swift-5-5

profile
iOS 개발

0개의 댓글