You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Swift는 구조화된 방식으로 비동기 및 병렬 코드를 작성할 수 있도록 내장된 지원 기능을 가지고 있다
비동기 코드는 일시 중단되었다가 나중에 재개될 수 있지만 한 번에 한 부분만 실행된다
프로그램에서 코드를 일시 중단 및 재개하면
네트워크를 통해 데이터를 가져오거나 파일을 구문 분석하는 것과 같은 장기 실행 작업을 계속하면서
UI 업데이트와 같은 단기 작업을 계속 진행할 수 있다
병렬 코드는 여러 개의 코드가 동시에 실행되는 것을 의미한다
ex. 4코어 프로세서가 장착된 컴퓨터는 4개의 코드를 동시에 실행할 수 있으며 각 코어는 하나의 작업을 수행한다
병렬 코드와 비동기 코드를 함께 사용하는 프로그램은 한 번에 여러 개의 연산을 수행하는데,
외부 시스템을 기다리는 작업을 중단하고 메모리 세이프 방식으로 해당 코드를 쉽게 작성할 수 있도록 한다
병렬 또는 비동기 코드의 추가적인 스케줄링 유연성은 시간 / 공간 복잡도의 비용을 수반한다
Swift에서는 일부 컴파일 시간 확인이 가능한 방식으로 의도를 명확히 표현할 수 있다
ex. actor 키워드를 사용하여 mutable 상태에 안전하게 접근할 수 있다
그러나, 느리거나 버그가 있는 코드에 동시성을 추가하는 것이 빠르거나 정확해진다는 보장은 아니며,
동시성을 추가하면 부가적으로 코드를 디버깅하기가 더 어려워질 수 있다
하지만 이런 단점을 넘어 동시적으로 실행되어야 하는 코드를 위해 Swift의 언어 수준의 지원을 사용하는 것은
Swift가 컴파일 시 문제를 파악하는 데 도움을 줄 수 있다
해당 챕터의 나머지 부분에서는 동시성이라는 용어를 사용하여 비동기 코드와 병렬 코드의 일반적인 조합을 구성하는 방법을 배운다
이전에 동시 코드를 작성한 적이 있는 경우 스레드 작업에 익숙할 수 있다
Swift의 동시성 모델은 스레드 위에 구축되지만 직접 상호 작용하지는 않는다
Swift의 비동기 함수는 실행 중인 스레드를 포기할 수 있으며,
이는 첫 번째 함수가 차단되는 동안 다른 비동기 함수를 해당 스레드에서 실행할 수 있게 한다
Swift는 비동기 기능이 재개될 때 해당 기능이 어떤 스레드에서 실행될지에 대해 보장하지 않는다
스위프트의 언어 지원을 사용하지 않고 동시 코드를 작성하는 것은 가능하지만,
그러한 코드는 읽기 더 어려운 경향이 있다
다음 코드는 사진 이름 목록을 다운로드하고 해당 목록의 첫 번째 사진을 다운로드하여 사용자에게 해당 사진을 표시한다
위처럼 간단한 경우에도 코드는 일련의 completion 핸들러로 작성되어야 하기 때문에 중첩되도록 클로저를 작성하게 된다
해당 스타일에서, 깊은 중첩이 있는 더 복잡한 코드는 빠르게 다루기 어려워질 수 있다
비동기 함수 정의 및 호출
비동기 함수 또는 메서드는 실행 도중에 중단될 수 있는 특별한 종류의 함수 또는 메서드다
완료까지 실행되거나, 오류를 발생시키거나, 반환 값이 없는 일반적인 동기 함수 및 방법과는 대조적
비동기 함수는 여전히 위의 세 가지 중 하나를 수행하지만,
작업이 무언가를 기다리고 있을 때 중간에 일시 정지할 수도 있다
비동기 함수의 본문 내에서 실행을 일시 중단할 수 있는 각 위치를 표시할 수 있다
함수가 비동기임을 나타내려면 매개 변수 뒤에 async 키워드를 입력한다
이는 throwing function를 표시하는 데 사용하는 방법과 유사
함수가 값을 반환하면 반환 화살표(->) 앞에 async를 키워드를 입력한다
예제는 갤러리에서 사진 이름을 가져오는 방법을 나타낸다
func listPhotos(inGallery name:String)async->[String]{letresult= // ... some asynchronous networking code ...
return result
}
비동기 및 에러 throw가 모두 가능한 함수의 경우 투척 전에 async를 작성한다
비동기 함수를 호출하면 해당 함수가 반환 될 때까지 실행이 일시 중단된다
함수 호출 앞에 await를 써서 가능한 중단 지점을 표시
throwing function을 호출할 때 오류가 있을 경우
프로그램 흐름의 가능한 변경을 표시하기 위해 try 키워드를 쓰는 것과 유사
비동기 메서드에서 실행 흐름은 다른 비동기 메서드를 호출할 때만 일시 중단된다
일시 중단은 암묵적이거나 선제적이지 않기에
따라서, 가능한 모든 일시 중단 지점이 await로 표시되야한다
listPhotos(inGallery:) 및 downloadPhoto(named:) 함수는 모두 네트워크 요청을 해야 하므로
완료하는 데 비교적 오랜 시간이 걸릴 수 있다.
반환 화살표 전에 async 키워드를 작성하여 둘 다 비동기화하면
해당 코드가 사진이 준비되기를 기다리는 동안 앱의 나머지 코드가 계속 실행될 수 있다
위의 예제의 동시 속성을 이해하기 위해 실행 순서를 살펴보자
코드는 첫 번째 줄부터 실행되기 시작하여 첫 번째 await까지 실행됩니다.
listPhotos(inGallery:) 함수를 호출하고 해당 함수가 반환될 때까지 기다리는 동안 실행을 일시 중단합니다.
해당 코드의 실행이 일시 중단되는 동안 같은 프로그램에서 다른 일부 동시 코드가 실행됩니다.
예를 들어 장시간 실행되는 백그라운드 작업이 새 사진 갤러리 목록을 계속 업데이트할 수 있습니다.
이 코드는 대기 상태로 표시된 다음 정지 지점까지 또는 완료될 때까지 실행됩니다.
listPhotos(inGallery:)가 반환된 후 코드는 해당 지점에서 시작하여 실행을 계속합니다.
photoNames에 반환된 값을 할당합니다.
sortedNames과 name을 정의하는 행은 일반 동기 코드입니다.
해당 라인에는 await라고 표시된 것이 없기 때문에, 가능한 정지 지점이 없습니다.
다음 await 키워드는 downloadPhoto(named:) 함수에 대한 호출을 표시합니다.
해당 코드는 함수가 반환될 때까지 실행을 다시 일시 중지하여 다른 동시 코드를 실행할 수 있는 기회를 제공합니다.
downloadPhoto(named:)가 반환된 후 반환 값이 photo에 할당되고 show(_:)를 호출할 때 인수로 전달됩니다.
await로 표시된 코드의 중단 가능점은
비동기 함수가 반환되기를 기다리는 동안 현재 코드 조각이 실행을 일시 중지할 수 있음을 의미한다
이는 스레드 산출(yielding the thread)이라고도 불리는데,
Swift가 뒤에서 현재 스레드에서 코드 실행을 중단하고
대신 해당 스레드에서 다른 코드를 실행하기 때문이다
await 키워드가 있는 코드는 실행을 일시 중단할 수 있어야 하므로
프로그램의 특정 위치만 비동기 함수를 호출할 수 있다
아래 구조화되지 않은 동시성(Unstructured Concurrency) 챕터에 표시된 것처럼, 구조화되지 않은 하위 작업의 코드(child task)
중단 가능점 사이의 코드는 다른 동시 코드의 중단 가능성 없이 순차적으로 실행된다
아래 코드는 한 갤러리에서 다른 갤러리로 사진을 이동한다
letfirstPhoto=awaitlistPhotos(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:) 호출 사이에 다른 코드가 실행될 방법이 없다
그 시간 동안, firstPhoto가 두 갤러리에 나타나 앱의 불변성 중 하나를 일시적으로 깨뜨린다
해당 코드 덩어리가 나중에 추가될 때까지 기다리지 않아야 한다는 것을 더욱 명확히 하기 위해,
해당 코드를 동기 함수로 리팩터링 해보자
func move(_ photoName:String, from source:String, to destination:String){add(photoName, to: destination)remove(photoName, from: source)}
// ...
letfirstPhoto=awaitlistPhotos(inGallery:"Summer Vacation")[0]move(firstPhoto, from:"Summer Vacation", to:"Road Trip")
에제에서는 move(_:from:to:)는 동기 함수이므로 중단 가능점 await를 포함할 수 없다
앞으로 해당 함수에 동시 코드를 추가하여 중단 가능점을 도입하려고 하면
버그를 도입하는 대신 컴파일 타임 오류가 발생한다
Task.sleep(until:transference:clock:) 메서드는 동시성이 어떻게 작동하는지 배우기 위해 간단한 코드를 작성할 때 유용하다
해당 메서드는 아무것도 하지 않지만 적어도 주어진 나노초 동안 기다렸다가 돌아온다
다음 코드는 sleep(until:tolerance:clock:)를 사용하여 네트워크 작업 대기를 시뮬레이션하는 listPhotos(inGallery:) 함수 구성이다
func listPhotos(inGallery name:String)async->[String]{letresult= // ... some asynchronous networking code ...
return result
}
앞서 정의했던 listPhotos(inGallery:) 함수는 배열의 모든 요소가 준비되면 한 번에 전체 배열을 비동기적으로 반환한다
또 다른 접근법은 비동기 시퀀스를 사용하여 한 번에 컬렉션의 한 요소를 기다리는 것이다
다음 코드는 비동기 시퀀스에 대해 이터레이션하는 예제다
import Foundation
lethandle=FileHandle.standardInput
fortryawaitlinein handle.bytes.lines {print(line)}
일반적인 for-in 루프를 사용하는 대신, 위의 예제는 for 뒤에 await 키워드를 쓴다
비동기 함수를 호출할 때와 마찬가지로 쓰기 await는 중단 가능점을 나타낸다
대기 루프(for-wait-in loop)는 다음 요소가 사용 가능해지기를 기다리면서, 각 반복의 시작에서 실행을 일시 중단할 수 있다
Sequence 프로토콜을 준수하도록 하여, for-in 루프에서 특정 타입을 사용할 수 있는 것과 마찬가지로,
AsyncSequence 프로토콜을 준수하도록 하여, 대기 루프(for-wait-in loop)에서 특정 타입을 사용할 수 있다
병렬로 비동기 함수 호출
await를 함수 호출 시에 사용하여 비동기 함수를 호출하면 한 번에 하나의 코드만 실행된다
비동기 코드가 실행되는 동안, 호출자는 다음 코드 행으로 이동하기 전에 해당 코드가 완료될 때까지 기다린다
아래 코드는 갤러리에서 처음 세 장의 사진을 가져오기 위해, 다음과 같이 사진 downloadPhoto(named:) 함수 대한 세 번의 호출을 기다리고 나서 구성하는 예시
위 예제에서는 세 번의 downloadPhoto(named:) 호출이 이전 호출이 완료될 때까지 기다리지 않고 시작된다
사용 가능한 시스템 리소스가 충분하면 동시에 실행 가능
세 번의 함수 호출은 결과를 기다리기 위해 코드가 일시 중단되지 않기 때문에 await로 표시되지 않는다
대신 photos가 정의된 줄까지 실행이 계속된다
이 시점에서 프로그램은 이러한 비동기 호출의 결과가 필요하므로
세 사진 모두 다운로드가 완료될 때까지 실행을 일시 중지하도록 대기한다
위에서 두 가지 접근 방식의 차이점을 생각해 볼 수 있는 방법은 다음과 같다
다음 줄의 코드가 해당 함수의 결과에 따라 달라지는 경우 비동기 함수를 await로 호출한다.
이렇게 하면 순차적으로 수행되는 작업이 만들어진다.
코드 뒷부분까지 결과가 필요하지 않을 때 비동기 함수를 async-let로 호출한다.
이것은 병렬로 수행될 수 있는 작업을 만든다.
await 와 async-let 둘 다 다른 코드가 일시 중단된 동안 실행될 수 있다.
두 경우 모두 중단 가능점을 await로 표시하여, 필요한 경우 비동기 기능이 반환될 때까지 실행이 일시 중지됨을 나타낸다.
또한, 이 두 가지 접근 방식을 동일한 코드 안에서 혼합하여 사용할 수도 있다
태스크과 태스크 그룹
태스크는 프로그램의 일부로 비동기식으로 실행할 수 있는 작업 단위다
모든 비동기 코드는 어떤 태스크의 일부로 실행된다
이전 섹션에서 설명한 async-let 구문은 자식 태스크를 만든다
태스크 그룹을 생성하고 자식 태스크를 해당 그룹에 추가할 수도 있으므로
우선 순위 및 취소를 보다 효과적으로 제어할 수 있으며 동적인 숫자의 태스크들을 생성할 수 있다
태스크는 계층 구조로 정렬된다
태스크 그룹의 각 태스크에는 동일한 부모 태스크가 있으며 각 태스크에는 자식 태스크가 있을 수 있다
태스크와 태스크 그룹 간의 명확한 관계 때문에 이 접근 방식을 구조화된 동시성이라고 한다
정확성에 대한 책임을 일부 지더라도 작업 간의 명시적인 부모-자식 관계를 통해
Swift는 취소 전파와 같은 동작을 처리하고 컴파일 시 Swift가 일부 오류를 탐지할 수 있다
이전 섹션에서 설명한 구조화된 동시성 접근 방식 외에도, Swift는 구조화되지 않은 동시성 또한 지원한다
태스크 그룹의 일부인 태스크와 달리 조직화 되지 않은 태스크에는 부모 태스크가 없다
사용자는 프로그램이 필요로 하는 모든 방식으로 조직화 되지 않은 태스크를 관리할 수 있는 완전한 유연성을 갖추고 있을 뿐 아니라
이러한 태스크의 정확성에 대해서도 전적으로 책임을 져야한다
현재 actor에서 실행되는 조직화 되지 않은 태스크를 만들려면 Task.init(priority:operation:) 이니셜라이저를 호출한다
현재 actor의 일부가 아닌 조직화 되지 않은 태스크(특히 분리된 태스크)를 만들려면 Task.detached(priority:operation:) 메서드를 호출한다
이 두 작업은 모두 결과를 기다리거나 취소할 수 있는 상호 작용이 가능한 태스크를 반환한다
letnewPhoto= // ... some photo data ...
let handle =Task{returnawaitadd(newPhoto, toGalleryNamed:"Spring Adventures")}letresult=await handle.value
스위프트의 동시성은 cooperative cancellation 모델을 사용한다
각 태스크는 실행 시 적절한 지점에서 취소되었는지 확인하고 적절한 방식으로 취소에 대응한다
작업에 따라 일반적으로 다음 중 하나를 의미한다
CancellationError와 같은 에러 발생
nil 또는 빈 컬렉션을 반환
부분적으로 완료된 작업 반환
취소 여부를 확인하려면,
작업이 취소된 경우 CancellationError가 발생하는 Task.checkCancel()을 호출하거나, Task.isCanceled의 값을 확인한다
그 이후, 작성한 코드를 취소 처리하면 된다
ex. 갤러리에서 사진을 다운로드하는 태스크는 부분 다운로드된 데이터를 삭제하고 네트워크 연결을 닫아야 한다
태스크를 사용하여 프로그램을 분리된 동시 조각으로 분할할 수 있다
태스크가 서로 분리되어 있기 때문에 동시에 안전하게 실행할 수 있지만
태스크 간에 일부 정보를 공유해야 하는 경우도 있다
이럴 경우 액터를 사용하면 동시 코드 간에 안전하게 정보를 공유할 수 있다
클래스와 마찬가지로 액터는 참조 타입이므로
앞서 다뤗던 Classes Are Reference Types에서 값 타입과 참조 타입의 비교는 클래스뿐만 아니라 액터에도 적용된다
클래스와 달리 액터는 한 번에 하나의 작업만 변경 가능한 상태에 액세스할 수 있도록 허용하므로
여러 태스크의 코드가 액터의 동일한 인스턴스와 상호 작용하는 것이 안전하다
아래 예제를 보면, 온도를 기록하는 actor가 있다
actor 키워드를 사용하여 액터를 시작하고 그 정의를 {} 쌍으로 구성한다
TemperatureLogger 액터는 액터 외부의 다른 코드가 액세스할 수 있는 프로퍼티를 가지고 있으며,
액터 내부의 코드만 최대값을 업데이트할 수 있도록 max 속성을 private(set)으로 제한한다
구조체 및 클래스와 동일한 이니셜라이저 구문을 사용하여 액터의 인스턴스를 만든다
액터의 프로퍼티나 메서드에 액세스할 때, await 키워드를 사용하여 잠재적 중단점을 표시한다
letlogger=TemperatureLogger(label:"Outdoors", measurement:25)print(await logger.max)
// Prints "25"
위 예에서는 logger.max에 액세스하는 것이 중단 가능점이다
액터는 한 번에 하나의 태스크만 자신의 mutable 상태에 접근할 수 있도록 허용하므로,
다른 태스크의 코드가 이미 logger와 상호 작용하고 있는 경우에는
해당 코드는 프로퍼티에 액세스하기 위해 대기하는 동안 일시 중단된다
대조적으로, 액터의 프로퍼티에 접근할 때, 액터의 내부 코드는 await 키워드를 작성하지 않는다
예를 들어, Temperature Logger를 새 온도로 업데이트하는 매서드는 다음과 같다
extensionTemperatureLogger{func update(with measurement:Int){
measurements.append(measurement)if measurement > max {
max = measurement
}}}
update(with:) 메서드가 이미 액터에서 실행 중이므로
max와 같은 프로퍼티에 대한 접근에 await를 표시하지 않는다
또한, 해당 메서드는 액터들이 한 번에 하나의 태스크만 허용하여 그들의 mutable한 상태와 상호 작용하게 하는 이유 중 하나를 보여준다
액터의 상태에 대한 일부 업데이트는 일시적으로 불변성을 깬다
Temperature Logger 액터는 온도 및 최대 온도 목록을 추적하고
새 측정을 기록할 때 최대 온도를 업데이트한다
업데이트 중 새 측정값을 measurements에 추가한 후 max를 업데이트하기 전에
Temperature Logger가 일시적으로 일관되지 않은 상태에 있다
여러 태스크가 동일한 인스턴스와 동시에 상호 작용하는 것을 방지하면
다음과 같은 이벤트 시퀀스와 같은 문제를 방지할 수 있다
코드는 update(with:) 메서드를 호출한다. 호출로 먼저 measurements 배열을 업데이트한다
코드가 max를 업데이트하기 전에 다른 곳의 코드는 최대값과 온도 배열을 읽는다
max을 변경하여 코드가 업데이트를 마친다
이럴 경우, 다른 곳에서 실행되는 코드는 잘못된 정보를 읽게 되는데,
이는 데이터가 일시적으로 유효하지 않은 update(with:)의 호출 중간에 액터에 대한 접근이 끼어들었기 때문이다
Swift 액터는 한 번에 하나의 작업만 허용하고
await를 통해 중단점을 표시한 위치에서만 코드가 중단될 수 있으므로
Swift 액터를 사용할 때 해당 문제를 방지할 수 있다
update(with:)에는 중단점이 없으므로 업데이트 중에 다른 코드가 데이터에 액세스할 수 없다
클래스의 인스턴스에서처럼 액터 외부에서 이러한 속성에 액세스하려고 하면 컴파일 시간 오류가 발생하게 된다
print(logger.max) // Error
액터의 프로퍼티가 액터의 분리된 로컬 상태의 일부이기 때문에
await 키워드 없이 logger.max에 액세스할 수 없다
Swift는 액터 내부의 코드만이 액터의 local state에 접근할 수 있다고 보장한다
이러한 보장을 actor isolation라고 한다
Sendable 타입
태스크 및 액터를 사용하여 프로그램을 안전하게 동시에 실행할 수 있는 조각으로 나눌 수 있다
태스크나 액터의 인스턴스 안에서 변수나 프로퍼티와 같이 mutable state를 포함하는 프로그램의 부분을 동시성 도메인(concurrency domain)이라고 한다
일부 종류의 데이터는 동시성 도메인(concurrency domain) 간에 공유할 수 없다
해당 데이터에 mutable state가 포함되어 있지만 중복 액세스로부터 보호되지는 않기 때문
한 동시성 도메인(concurrency domain)에서 다른 도메인으로 공유할 수 있는 타입을 전송 가능한 타입이라고 한다
예를 들어, 액터 메서드를 호출할 때 인수로 전달되거나 작업의 결과로 반환될 수 있다.
이번 챕터의 앞부분에서는 동시성 도메인 간에 전달되는 데이터에 대해
항상 안전하게 공유할 수 있는 단순한 값 타입을 사용하기 때문에 전송 가능성에 대해 논의하지 않았다
하지만, 일부 타입은 동시성 도메인 간에 전달하기에 안전하지 않다
예를 들어, mutable한 프로퍼티를 포함하고 이러한 프로퍼티에 대한 접근을 직렬화하지 않는 클래스는
다른 태스크 간에 해당 클래스의 인스턴스를 전달할 때 예측 불가능한 잘못된 결과를 초래할 수 있다
Sendable 프로토콜 준수하여 타입을 보내기 가능으로 표시하면,
해당 프로토콜은 어떠한 코드 요건도 가지고 있지 않지만,
Swift가 정한 의미론적 요건은 가지고 있다
일반적으로 타입을 보낼 수 있는 방법은 세 가지가 있다
값 타입이며, mutable state는 다른 전송 가능한 데이터로 구성된 경우
ex. 전송 가능한 프로퍼티가 저장된 구조체 / 전송 가능한 연관 값을 가진 열거형
타입에 mutable state가 없으며, immutable state는 다른 전송 가능한 데이터로 구성된 경우
ex. 읽기 전용 프로퍼티만 있는 구조체 또는 클래스
타입에 mutable state의 안전을 보장하는 코드가 있는 경우
ex. @mainactor로 표시된 클래스 / 특정 스레드 또는 Queue에서 해당 프로퍼티에 대한 접근을 직렬화하는 클래스
Swift는 구조화된 방식으로 비동기 및 병렬 코드를 작성할 수 있도록 내장된 지원 기능을 가지고 있다
비동기 코드는 일시 중단되었다가 나중에 재개될 수 있지만 한 번에 한 부분만 실행된다
프로그램에서 코드를 일시 중단 및 재개하면
네트워크를 통해 데이터를 가져오거나 파일을 구문 분석하는 것과 같은 장기 실행 작업을 계속하면서
UI 업데이트와 같은 단기 작업을 계속 진행할 수 있다
병렬 코드는 여러 개의 코드가 동시에 실행되는 것을 의미한다
ex. 4코어 프로세서가 장착된 컴퓨터는 4개의 코드를 동시에 실행할 수 있으며 각 코어는 하나의 작업을 수행한다
병렬 코드와 비동기 코드를 함께 사용하는 프로그램은 한 번에 여러 개의 연산을 수행하는데,
외부 시스템을 기다리는 작업을 중단하고 메모리 세이프 방식으로 해당 코드를 쉽게 작성할 수 있도록 한다
병렬 또는 비동기 코드의 추가적인 스케줄링 유연성은 시간 / 공간 복잡도의 비용을 수반한다
Swift에서는 일부 컴파일 시간 확인이 가능한 방식으로 의도를 명확히 표현할 수 있다
ex. actor 키워드를 사용하여 mutable 상태에 안전하게 접근할 수 있다
그러나, 느리거나 버그가 있는 코드에 동시성을 추가하는 것이 빠르거나 정확해진다는 보장은 아니며,
동시성을 추가하면 부가적으로 코드를 디버깅하기가 더 어려워질 수 있다
하지만 이런 단점을 넘어 동시적으로 실행되어야 하는 코드를 위해 Swift의 언어 수준의 지원을 사용하는 것은
Swift가 컴파일 시 문제를 파악하는 데 도움을 줄 수 있다
해당 챕터의 나머지 부분에서는 동시성이라는 용어를 사용하여 비동기 코드와 병렬 코드의 일반적인 조합을 구성하는 방법을 배운다
스위프트의 언어 지원을 사용하지 않고 동시 코드를 작성하는 것은 가능하지만,
그러한 코드는 읽기 더 어려운 경향이 있다
다음 코드는 사진 이름 목록을 다운로드하고 해당 목록의 첫 번째 사진을 다운로드하여 사용자에게 해당 사진을 표시한다
위처럼 간단한 경우에도 코드는 일련의 completion 핸들러로 작성되어야 하기 때문에 중첩되도록 클로저를 작성하게 된다
해당 스타일에서, 깊은 중첩이 있는 더 복잡한 코드는 빠르게 다루기 어려워질 수 있다
비동기 함수 정의 및 호출
비동기 함수 또는 메서드는 실행 도중에 중단될 수 있는 특별한 종류의 함수 또는 메서드다
완료까지 실행되거나, 오류를 발생시키거나, 반환 값이 없는 일반적인 동기 함수 및 방법과는 대조적
비동기 함수는 여전히 위의 세 가지 중 하나를 수행하지만,
작업이 무언가를 기다리고 있을 때 중간에 일시 정지할 수도 있다
비동기 함수의 본문 내에서 실행을 일시 중단할 수 있는 각 위치를 표시할 수 있다
함수가 비동기임을 나타내려면 매개 변수 뒤에 async 키워드를 입력한다
이는 throwing function를 표시하는 데 사용하는 방법과 유사
함수가 값을 반환하면 반환 화살표(->) 앞에 async를 키워드를 입력한다
예제는 갤러리에서 사진 이름을 가져오는 방법을 나타낸다
비동기 및 에러 throw가 모두 가능한 함수의 경우 투척 전에 async를 작성한다
비동기 함수를 호출하면 해당 함수가 반환 될 때까지 실행이 일시 중단된다
함수 호출 앞에 await를 써서 가능한 중단 지점을 표시
throwing function을 호출할 때 오류가 있을 경우
프로그램 흐름의 가능한 변경을 표시하기 위해 try 키워드를 쓰는 것과 유사
비동기 메서드에서 실행 흐름은 다른 비동기 메서드를 호출할 때만 일시 중단된다
일시 중단은 암묵적이거나 선제적이지 않기에
따라서, 가능한 모든 일시 중단 지점이 await로 표시되야한다
아래 코드는 갤러리에 있는 모든 사진의 이름을 가져온 다음 첫 번째 사진을 표시한다
listPhotos(inGallery:) 및 downloadPhoto(named:) 함수는 모두 네트워크 요청을 해야 하므로
완료하는 데 비교적 오랜 시간이 걸릴 수 있다.
반환 화살표 전에 async 키워드를 작성하여 둘 다 비동기화하면
해당 코드가 사진이 준비되기를 기다리는 동안 앱의 나머지 코드가 계속 실행될 수 있다
위의 예제의 동시 속성을 이해하기 위해 실행 순서를 살펴보자
코드는 첫 번째 줄부터 실행되기 시작하여 첫 번째 await까지 실행됩니다.
listPhotos(inGallery:) 함수를 호출하고 해당 함수가 반환될 때까지 기다리는 동안 실행을 일시 중단합니다.
해당 코드의 실행이 일시 중단되는 동안 같은 프로그램에서 다른 일부 동시 코드가 실행됩니다.
예를 들어 장시간 실행되는 백그라운드 작업이 새 사진 갤러리 목록을 계속 업데이트할 수 있습니다.
이 코드는 대기 상태로 표시된 다음 정지 지점까지 또는 완료될 때까지 실행됩니다.
listPhotos(inGallery:)가 반환된 후 코드는 해당 지점에서 시작하여 실행을 계속합니다.
photoNames에 반환된 값을 할당합니다.
sortedNames과 name을 정의하는 행은 일반 동기 코드입니다.
해당 라인에는 await라고 표시된 것이 없기 때문에, 가능한 정지 지점이 없습니다.
다음 await 키워드는 downloadPhoto(named:) 함수에 대한 호출을 표시합니다.
해당 코드는 함수가 반환될 때까지 실행을 다시 일시 중지하여 다른 동시 코드를 실행할 수 있는 기회를 제공합니다.
downloadPhoto(named:)가 반환된 후 반환 값이 photo에 할당되고 show(_:)를 호출할 때 인수로 전달됩니다.
await로 표시된 코드의 중단 가능점은
비동기 함수가 반환되기를 기다리는 동안 현재 코드 조각이 실행을 일시 중지할 수 있음을 의미한다
이는 스레드 산출(yielding the thread)이라고도 불리는데,
Swift가 뒤에서 현재 스레드에서 코드 실행을 중단하고
대신 해당 스레드에서 다른 코드를 실행하기 때문이다
await 키워드가 있는 코드는 실행을 일시 중단할 수 있어야 하므로
프로그램의 특정 위치만 비동기 함수를 호출할 수 있다
중단 가능점 사이의 코드는 다른 동시 코드의 중단 가능성 없이 순차적으로 실행된다
아래 코드는 한 갤러리에서 다른 갤러리로 사진을 이동한다
add(:toGallery:)와 remove(:fromGallery:) 호출 사이에 다른 코드가 실행될 방법이 없다
그 시간 동안, firstPhoto가 두 갤러리에 나타나 앱의 불변성 중 하나를 일시적으로 깨뜨린다
해당 코드 덩어리가 나중에 추가될 때까지 기다리지 않아야 한다는 것을 더욱 명확히 하기 위해,
해당 코드를 동기 함수로 리팩터링 해보자
에제에서는 move(_:from:to:)는 동기 함수이므로 중단 가능점 await를 포함할 수 없다
앞으로 해당 함수에 동시 코드를 추가하여 중단 가능점을 도입하려고 하면
버그를 도입하는 대신 컴파일 타임 오류가 발생한다
비동기 시퀀스
앞서 정의했던 listPhotos(inGallery:) 함수는 배열의 모든 요소가 준비되면 한 번에 전체 배열을 비동기적으로 반환한다
또 다른 접근법은 비동기 시퀀스를 사용하여 한 번에 컬렉션의 한 요소를 기다리는 것이다
다음 코드는 비동기 시퀀스에 대해 이터레이션하는 예제다
일반적인 for-in 루프를 사용하는 대신, 위의 예제는 for 뒤에 await 키워드를 쓴다
비동기 함수를 호출할 때와 마찬가지로 쓰기 await는 중단 가능점을 나타낸다
대기 루프(for-wait-in loop)는 다음 요소가 사용 가능해지기를 기다리면서, 각 반복의 시작에서 실행을 일시 중단할 수 있다
Sequence 프로토콜을 준수하도록 하여, for-in 루프에서 특정 타입을 사용할 수 있는 것과 마찬가지로,
AsyncSequence 프로토콜을 준수하도록 하여, 대기 루프(for-wait-in loop)에서 특정 타입을 사용할 수 있다
병렬로 비동기 함수 호출
await를 함수 호출 시에 사용하여 비동기 함수를 호출하면 한 번에 하나의 코드만 실행된다
비동기 코드가 실행되는 동안, 호출자는 다음 코드 행으로 이동하기 전에 해당 코드가 완료될 때까지 기다린다
아래 코드는 갤러리에서 처음 세 장의 사진을 가져오기 위해, 다음과 같이 사진 downloadPhoto(named:) 함수 대한 세 번의 호출을 기다리고 나서 구성하는 예시
해당 접근 방식에는 중요한 단점이 있다
다운로드가 비동기식이고 진행 중에 다른 작업을 수행할 수 있지만,
downloadPhoto(named:)이 한 번에 한 번씩 불려 실행된다
한 사진이 다운로드가 완전히 다운로드 되고 나서야, 다음 사진이 다운로드 시작된다
하지만, 이러한 병렬적인 작업들을 직렬로 기다릴 필요가 없다
각 사진은 독립적으로 또는 동시에 다운로드 하도록 구성이 가능하다
비동기 함수를 호출하여 주변의 코드와 병렬로 실행되도록 하려면
상수를 정의할 때 let 앞에 async를 쓴 다음 상수를 사용할 때마다 await 키워드를 쓰면된다
위 예제에서는 세 번의 downloadPhoto(named:) 호출이 이전 호출이 완료될 때까지 기다리지 않고 시작된다
사용 가능한 시스템 리소스가 충분하면 동시에 실행 가능
세 번의 함수 호출은 결과를 기다리기 위해 코드가 일시 중단되지 않기 때문에 await로 표시되지 않는다
대신 photos가 정의된 줄까지 실행이 계속된다
이 시점에서 프로그램은 이러한 비동기 호출의 결과가 필요하므로
세 사진 모두 다운로드가 완료될 때까지 실행을 일시 중지하도록 대기한다
위에서 두 가지 접근 방식의 차이점을 생각해 볼 수 있는 방법은 다음과 같다
다음 줄의 코드가 해당 함수의 결과에 따라 달라지는 경우 비동기 함수를 await로 호출한다.
이렇게 하면 순차적으로 수행되는 작업이 만들어진다.
코드 뒷부분까지 결과가 필요하지 않을 때 비동기 함수를 async-let로 호출한다.
이것은 병렬로 수행될 수 있는 작업을 만든다.
await 와 async-let 둘 다 다른 코드가 일시 중단된 동안 실행될 수 있다.
두 경우 모두 중단 가능점을 await로 표시하여, 필요한 경우 비동기 기능이 반환될 때까지 실행이 일시 중지됨을 나타낸다.
또한, 이 두 가지 접근 방식을 동일한 코드 안에서 혼합하여 사용할 수도 있다
태스크과 태스크 그룹
태스크는 프로그램의 일부로 비동기식으로 실행할 수 있는 작업 단위다
모든 비동기 코드는 어떤 태스크의 일부로 실행된다
이전 섹션에서 설명한 async-let 구문은 자식 태스크를 만든다
태스크 그룹을 생성하고 자식 태스크를 해당 그룹에 추가할 수도 있으므로
우선 순위 및 취소를 보다 효과적으로 제어할 수 있으며 동적인 숫자의 태스크들을 생성할 수 있다
태스크는 계층 구조로 정렬된다
태스크 그룹의 각 태스크에는 동일한 부모 태스크가 있으며 각 태스크에는 자식 태스크가 있을 수 있다
태스크와 태스크 그룹 간의 명확한 관계 때문에 이 접근 방식을 구조화된 동시성이라고 한다
정확성에 대한 책임을 일부 지더라도 작업 간의 명시적인 부모-자식 관계를 통해
Swift는 취소 전파와 같은 동작을 처리하고 컴파일 시 Swift가 일부 오류를 탐지할 수 있다
TaskGroup 페이지에서 더 알아 볼 수 있다
1) 조직화 되지 않은 동시성
이전 섹션에서 설명한 구조화된 동시성 접근 방식 외에도, Swift는 구조화되지 않은 동시성 또한 지원한다
태스크 그룹의 일부인 태스크와 달리 조직화 되지 않은 태스크에는 부모 태스크가 없다
사용자는 프로그램이 필요로 하는 모든 방식으로 조직화 되지 않은 태스크를 관리할 수 있는 완전한 유연성을 갖추고 있을 뿐 아니라
이러한 태스크의 정확성에 대해서도 전적으로 책임을 져야한다
현재 actor에서 실행되는 조직화 되지 않은 태스크를 만들려면 Task.init(priority:operation:) 이니셜라이저를 호출한다
현재 actor의 일부가 아닌 조직화 되지 않은 태스크(특히 분리된 태스크)를 만들려면 Task.detached(priority:operation:) 메서드를 호출한다
이 두 작업은 모두 결과를 기다리거나 취소할 수 있는 상호 작용이 가능한 태스크를 반환한다
Task 페이지에서 더 알아 볼 수 있다
2) 태스크 취소하기
스위프트의 동시성은 cooperative cancellation 모델을 사용한다
각 태스크는 실행 시 적절한 지점에서 취소되었는지 확인하고 적절한 방식으로 취소에 대응한다
작업에 따라 일반적으로 다음 중 하나를 의미한다
CancellationError와 같은 에러 발생
nil 또는 빈 컬렉션을 반환
부분적으로 완료된 작업 반환
취소 여부를 확인하려면,
작업이 취소된 경우 CancellationError가 발생하는 Task.checkCancel()을 호출하거나,
Task.isCanceled의 값을 확인한다
그 이후, 작성한 코드를 취소 처리하면 된다
ex. 갤러리에서 사진을 다운로드하는 태스크는 부분 다운로드된 데이터를 삭제하고 네트워크 연결을 닫아야 한다
취소를 수동으로 전파하려면 Task.cancel()을 호출한다
Actors
태스크를 사용하여 프로그램을 분리된 동시 조각으로 분할할 수 있다
태스크가 서로 분리되어 있기 때문에 동시에 안전하게 실행할 수 있지만
태스크 간에 일부 정보를 공유해야 하는 경우도 있다
이럴 경우 액터를 사용하면 동시 코드 간에 안전하게 정보를 공유할 수 있다
클래스와 마찬가지로 액터는 참조 타입이므로
앞서 다뤗던 Classes Are Reference Types에서 값 타입과 참조 타입의 비교는 클래스뿐만 아니라 액터에도 적용된다
클래스와 달리 액터는 한 번에 하나의 작업만 변경 가능한 상태에 액세스할 수 있도록 허용하므로
여러 태스크의 코드가 액터의 동일한 인스턴스와 상호 작용하는 것이 안전하다
아래 예제를 보면, 온도를 기록하는 actor가 있다
actor 키워드를 사용하여 액터를 시작하고 그 정의를 {} 쌍으로 구성한다
TemperatureLogger 액터는 액터 외부의 다른 코드가 액세스할 수 있는 프로퍼티를 가지고 있으며,
액터 내부의 코드만 최대값을 업데이트할 수 있도록 max 속성을 private(set)으로 제한한다
구조체 및 클래스와 동일한 이니셜라이저 구문을 사용하여 액터의 인스턴스를 만든다
액터의 프로퍼티나 메서드에 액세스할 때, await 키워드를 사용하여 잠재적 중단점을 표시한다
위 예에서는 logger.max에 액세스하는 것이 중단 가능점이다
액터는 한 번에 하나의 태스크만 자신의 mutable 상태에 접근할 수 있도록 허용하므로,
다른 태스크의 코드가 이미 logger와 상호 작용하고 있는 경우에는
해당 코드는 프로퍼티에 액세스하기 위해 대기하는 동안 일시 중단된다
대조적으로, 액터의 프로퍼티에 접근할 때, 액터의 내부 코드는 await 키워드를 작성하지 않는다
예를 들어, Temperature Logger를 새 온도로 업데이트하는 매서드는 다음과 같다
update(with:) 메서드가 이미 액터에서 실행 중이므로
max와 같은 프로퍼티에 대한 접근에 await를 표시하지 않는다
또한, 해당 메서드는 액터들이 한 번에 하나의 태스크만 허용하여 그들의 mutable한 상태와 상호 작용하게 하는 이유 중 하나를 보여준다
액터의 상태에 대한 일부 업데이트는 일시적으로 불변성을 깬다
Temperature Logger 액터는 온도 및 최대 온도 목록을 추적하고
새 측정을 기록할 때 최대 온도를 업데이트한다
업데이트 중 새 측정값을 measurements에 추가한 후 max를 업데이트하기 전에
Temperature Logger가 일시적으로 일관되지 않은 상태에 있다
여러 태스크가 동일한 인스턴스와 동시에 상호 작용하는 것을 방지하면
다음과 같은 이벤트 시퀀스와 같은 문제를 방지할 수 있다
코드는 update(with:) 메서드를 호출한다. 호출로 먼저 measurements 배열을 업데이트한다
코드가 max를 업데이트하기 전에 다른 곳의 코드는 최대값과 온도 배열을 읽는다
max을 변경하여 코드가 업데이트를 마친다
이럴 경우, 다른 곳에서 실행되는 코드는 잘못된 정보를 읽게 되는데,
이는 데이터가 일시적으로 유효하지 않은 update(with:)의 호출 중간에 액터에 대한 접근이 끼어들었기 때문이다
Swift 액터는 한 번에 하나의 작업만 허용하고
await를 통해 중단점을 표시한 위치에서만 코드가 중단될 수 있으므로
Swift 액터를 사용할 때 해당 문제를 방지할 수 있다
update(with:)에는 중단점이 없으므로 업데이트 중에 다른 코드가 데이터에 액세스할 수 없다
클래스의 인스턴스에서처럼 액터 외부에서 이러한 속성에 액세스하려고 하면 컴파일 시간 오류가 발생하게 된다
액터의 프로퍼티가 액터의 분리된 로컬 상태의 일부이기 때문에
await 키워드 없이 logger.max에 액세스할 수 없다
Swift는 액터 내부의 코드만이 액터의 local state에 접근할 수 있다고 보장한다
이러한 보장을 actor isolation라고 한다
Sendable 타입
태스크 및 액터를 사용하여 프로그램을 안전하게 동시에 실행할 수 있는 조각으로 나눌 수 있다
태스크나 액터의 인스턴스 안에서 변수나 프로퍼티와 같이 mutable state를 포함하는 프로그램의 부분을 동시성 도메인(concurrency domain)이라고 한다
일부 종류의 데이터는 동시성 도메인(concurrency domain) 간에 공유할 수 없다
해당 데이터에 mutable state가 포함되어 있지만 중복 액세스로부터 보호되지는 않기 때문
한 동시성 도메인(concurrency domain)에서 다른 도메인으로 공유할 수 있는 타입을 전송 가능한 타입이라고 한다
예를 들어, 액터 메서드를 호출할 때 인수로 전달되거나 작업의 결과로 반환될 수 있다.
이번 챕터의 앞부분에서는 동시성 도메인 간에 전달되는 데이터에 대해
항상 안전하게 공유할 수 있는 단순한 값 타입을 사용하기 때문에 전송 가능성에 대해 논의하지 않았다
하지만, 일부 타입은 동시성 도메인 간에 전달하기에 안전하지 않다
예를 들어, mutable한 프로퍼티를 포함하고 이러한 프로퍼티에 대한 접근을 직렬화하지 않는 클래스는
다른 태스크 간에 해당 클래스의 인스턴스를 전달할 때 예측 불가능한 잘못된 결과를 초래할 수 있다
Sendable 프로토콜 준수하여 타입을 보내기 가능으로 표시하면,
해당 프로토콜은 어떠한 코드 요건도 가지고 있지 않지만,
Swift가 정한 의미론적 요건은 가지고 있다
일반적으로 타입을 보낼 수 있는 방법은 세 가지가 있다
값 타입이며, mutable state는 다른 전송 가능한 데이터로 구성된 경우
ex. 전송 가능한 프로퍼티가 저장된 구조체 / 전송 가능한 연관 값을 가진 열거형
타입에 mutable state가 없으며, immutable state는 다른 전송 가능한 데이터로 구성된 경우
ex. 읽기 전용 프로퍼티만 있는 구조체 또는 클래스
타입에 mutable state의 안전을 보장하는 코드가 있는 경우
ex. @mainactor로 표시된 클래스 / 특정 스레드 또는 Queue에서 해당 프로퍼티에 대한 접근을 직렬화하는 클래스
의미론적 요건에 대한 자세한 리스트는 Sendable 프로토콜 페이지에서 더 알아볼 수 있다
보낼 수 있는 프로퍼티만 있는 구조체 및 보낼 수 있는 연관 값만 있는 열거형과 같이 일부 타입은 항상 보낼 수 있다
TemperatureReading은 보낼 수 있는 프로퍼티만 있는 구조체이고
구조체가 public 또는 @usableFromInline으로 표시되지 않았으므로 암시적으로 보낼 수 있다
Sendable 프로토콜 준수를 암시할 수 있으면 아래처럼 생략도 가능하다
The text was updated successfully, but these errors were encountered: