study record

[Swift] Concurrency - 1 본문

Swift/스위프트 정리

[Swift] Concurrency - 1

asong 2022. 10. 30. 21:49

Concurrency

Swift는 구조적인 방식으로 비동기적이며 병렬적인 코드를 쓰는 것을 지원하도록 만들어졌다. 비록 프로그램의 한 조각만이 한 번에 실행되지만 Asynchronous code는 중단되고 이후에 재개될 수 있다. 프로그램에서 중단되고 재개되는 코드는 UI 업데이트 같은 짧은 기간의 실행을 네트워크로 데이터 가져오기 또는 파일 파싱 같은 장시간의 작업을 하는 동안에 계속되게 한다. Parallel code는 동시에 코드의 다양한 조각을 실행하는 것을 의미한다. 예를들어, 4개의 코어 프로세서를 가진 하나의 컴퓨터가 동시에 코드의 4조각을 각각의 코어가 테스크들의 하나씩 이행하며 실행할 수 있는 것이다. parallel, asynchronous 프로그램은 한 번에 다양한 작업을 실행한다. 외부의 시스템을 기다리며 작동을 멈추기도 하고, 이것은 메모리 안전한 방식에서 코드를 쓰도록 해준다.

 

병렬이나 비동기 코드로부터 추가적인 스케줄링 유연성은 복잡성 증가의 비용을 동반한다. Swift는 예를 들어, 변경 가능한 상태에 안전하게 접근할 수 있는 actor를 사용할 수 있어 컴파일 타임을 체킹할 수 있게끔 하는, 의도를 표현할 수 있도록 해준다. 그러나, 느리고 버그가 있는 코드에 concurrency를 추가하는 것은 그것이 빠르거나 옳게 된다고 보장하지 않는다. 사실, concurrency를 추가하는 것은 코드를 디버그하기 힘들게 할지도 모른다. 그렇지만 concurrency가 필요한 코드에서 Swift의 언어 레벨 지원을 사용하는 것이 Swift가 컴파일 타임에 문제를 잡도록 도와줄 수 있다.

 

이 챕터에서 concurrency 용어를 비동기와 병렬 코드의 조합을 나타내기 위해 사용한다.

 

만약 concurrent 코드를 전에 써봤다면, 쓰레드와 함께 작업을 하는 것을 사용했을지도 모른다. Swift에서 concurrency 모델은 쓰레드의 가장 위에서 지어졌다. 그러나 그들과 직접적으로 상호작용하지 않는다. Swift에서 비동기 기능은 실행되는 동안 그 쓰레드에서 또다른 비동기 기능을 실행하면 첫번째 기능이 블락되며 쓰레드를 포기할 수 있다. 비동기 기능이 재개될 때, Swift는 기능이 계속될 쓰레드에 대해 어떤 보장도 하지 않는다.

 

 

비록 Swift의 언어 지원을 사용하는 것이 concurrent 코드를 쓰는 것이 가능하지만 코드는 읽기 더 어려워질 수 있다. 예를 들어, 다음의 코드는 사진 이름의 리스트를 다운로드한다. 리스트의 첫 사진을 다운로드하고 유저에게 사진을 보여준다.

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

이런 단순한 케이스에도, completion handlers의 연속으로서 코드가 쓰여지기 때문에 중첩 클로저를 쓰도록 되어버린다. 이러한 스타일에서, 깊은 중첩으로 더욱 복잡한 코드가 빠르게 커질 수 있다.

 

 

 

Defining and Calling Asynchronous Functions

비동기 함수나 비동기 메서드는 실행 도중에 중단될 수 있는 특별한 종류의 함수, 메서드이다. 이것은 종료까지 실행하거나 에러를 던지거나 리턴하지 않는 동기적 함수나 메서드와 반대된다. 비동기 메서드는 이 세가지 중 하나를 하지만 기다릴 때 그 중간에 멈출 수 있다. 비동기 함수 내부에, 실행이 중단된 지점들을 표시할 수 있다.

 

함수가 비동기임을 가리키기 위해, async 키워드를 파라미터 이후에 작성한다. 이것은 throws를 함수를 던지는 것을 표시하는 것과 유사하다. 만약 함수가 값을 리턴한다면, return arrow(->) 이전에 async를 작성한다. 예시는 갤러리에서 사진의 이름을 가져오는 방법이다.

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

비동기나 예외를 던지는 함수에서 throws 이전에 async를 쓴다.

 

비동기 메서드를 호출할 때, 실행은 메서드가 리턴할 때까지 중단된다. 중단 포인트를 표시하기 위해 호출의 앞에 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:), downloadPhoto(named:) 함수는 둘 다 네트워크 요청을 만드는 것이 필요하기 때문에, 그들은 완성하는데 상대적으로 시간이 걸릴 수 있다. 그들을 async를 씀으로써 비동기로 둘 다 만드는 것은 사진이 준비되기를 코드가 기다리는 동안 앱의 코드의 나머지를 계속 실행하게끔 한다.

 

위의 예제를 병렬적 본질을 이해하기 위해 실행의 가능한 순서를 제시한다.

 

1. 코드는 첫번째 라인으로부터 실행되고 첫번째 await으로 실행된다. 이것은 listPhotos(inGallery:)를 호출한다. 그리고 리턴할 때까지 기다리는 동안 실행이 멈춘다.

2. 코드의 실행이 멈춘 동안, 또다른 같은 프로그램의 병렬 코드가 실행한다. 예를 들어, 오래 걸리는 백그라운드 테스크가 새 포토 갤러리들의 리스트를 업데이트하는 것을 지속할 수 있다. 그 코드는 await으로 표시된 다음 중단 포인트까지 실행한다.

3. listPhotos(inGallery:)를 리턴한 후, 이 코드는 그 포인트에 실행 시작으로 계속된다. 이것은 photoNames에 리턴되어 값을 할당한다.

4. sortedNamesname은 동기 코드이다. await 표시가 되어있지 않기 때문에, 가능한 중단 포인트가 없다.

5. 다음 awaitdownloadPhoto(named:)함수이다. 이 코드는 함수가 리턴될 때까지 또다른 병렬 코드에게 실행할 기회를 주며 실행을 중단한다. 

6. downloadPhoto(named:)가 리턴된 후에, 리턴 값은 photo에 할당되고, show(_:)를 호출할 때 인자값을 넘겨진다.

 

await으로 표시된 코드에서 가능한 중지 포인트들은 비동기 함수가 리턴하는 것을 기다리는 동안 현재의 코드 조각이 실행을 멈출지 모른다는 것을 가리킨다. 이것은 yielding the thread라고 불린다. Swift는 현재 스레드에서 코드의 실행을 멈추고 대신에 그 스레드에 다른 코드를 실행시키기 때문이다. await과 함께 코드가 실행을 중지할 필요가 있기 때문에 오직 프로그램에서 특정 장소만 비동기 함수를 호출할 수 있다.

- 비동기 함수, 메서드, 프로퍼티의 body에서의 코드

- @main으로 표시된 struct, class, enum의 static main() 메서드에서의 코드

- 아래의 Unscrutured Concurrency에서 보여지는 구조화되지않은 child task의 코드

 

가능한 중지 포인트들 사이에서 코드는 또다른 병렬 코드로부터 가로막힐 가능성 없이 연속적으로 실행한다. 예를 들어, 아래의 코드는 한 갤러리로부터 다른 갤러리로 사진을 이동시킨다.

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(_:toGallery:)remove(_:fromGallery:) 사이에서 또다른 코드를 실행시킬 방법은 없다. 그 시간 동안 첫번 째 사진은 두 갤러리 모두에서 나타나고, 일시적으로 앱의 불변을 해친다. 이 코드들을 더 명확하게 하는 것은 미래에 await을 더하지 않는 것이다. 너는 동기 함수로 코드를 리팩토링할 수 있다.

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

위의 예제에서, move(_:from:to:) 함수가 동기함수이기 때문에 가능한 중지 포인트를 포함할 수 없다. 미래에 만약 이 함수를 병렬 코드에 추가하고자 한다면, 가능한 중지 포인트를 도입하고자할때, 버그를 만나기 대신에 컴파일 타임 에러를 얻을 것이다.

 

Asynchronous Sequences

listPhotos(inGallery:) 함수는 배열의 모든 요소가 준비된 후에 한 번에 전체의 배열을 비동기적으로 리턴한다. 또다른 접근은 asynchronous sequence를 사용하여 한 번에 컬렉션의 한 요소를 기다리는 것이다. 

 

import Foundation

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

for-in 루프를 사용하는 것 대신에 예제는 for를 await과 함께 사용하였다. 비동기 함수를 호출할 때같이, await을 쓰는 것은 가능한 중지 포인트를 보여준다. for-await-in 루프는 잠재적으로 다음 요소가 이용가능해질때까지 기다리며 각 작업의 시작에서 실행을 중지한다.

 

같은 방식으로 for-await-in 루프를 AsyncSequence protocol을 채택함으로써 자신의 타입에 맞게 사용할 수 있다.

 

 

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html