동시성
Swift는 비동기적 및 병렬 코드를 구조화된 방법으로 내장지원 합니다. 비동기적 코드
는 한 번에 한 부분만 실행되지만 나중에 일시 중단했다가 다시 시작할 수 있습니다. 프로그램을 일시중단을 하고 재시작(Suspending and resuming)을 시키는 코드는 파일을 파싱하거나 네트워크를 통해 데이터를 가져오는 긴 시간 작업동안 빠르게 UI를 짧은 기간(short term)동안 업데이트 시킵니다.
병렬코드
는 동시에 여러 코드뭉치를 실행시키는것을 말합니다. — 예를들면, 4코어 프로세서 컴퓨터가 4가지일을 동시에 처리하는것과 같은일들입니다. 병렬 및 비동기적 코드는 여러일을 동시에
할 수있음을 뜻합니다.외부 시스템이 기다리는것을 막고 memory-safe
한 코드를 짤 수 있습니다.
병렬 또는 비동기적 코드는 복잡성 증가 비용 (a cost of increased complexity)
을 유연하게 해줍니다. Swift
를 사용하면 컴파일 시간 검사를 수행할 수 있습니다. 예를 들어, actor
를 사용하여 변경 가능한 상태에 안전하게 액세스할 수 있습니다. 그러나 동시성은 속도나 정확성은 버그유발 코드
에 대해서는 확신해줄 수는 없습니다. 다른말로는, 동시성을 추가하면 코드 디버깅
을 힘들게 할 수도 있습니다. 그러나, Swift
의 언어 레벨은 컴파일 시간에 이와같은 문제들을 잡을 수 있습니다.
이 챕터에서는 동시성
이란 주제로 동기적 및 병렬 코드에 대해 알아 보겠습니다.
Note만약 동시성(Concurrent) 코드를 이 글을 읽기 전에 작성했다면, 스레드에 접근하여 작업했을 수 있습니다. 스위프트에서의 이 동시성모델은 스레드의 최상단에 작업되었을것이지만, 직접적으로 작업은 하지 않았을겁니다. 스위프트에서의 비동기적 함수는 첫번째 함수가 한 스레드에서 작업하다 블락되었을때, 같은 스레드에서 다른 함수의 비동기적 처리를 할 수도 있습니다. (= data race)
아래와 같은 코드들은 가독성면으로 좋지 않은데 예제를 보자면,
위와 같은 간단한 예제에도 nested closure
와 completion handler
들로 인해 가독성이 떨어집니다.
비동기적 함수의 정의와 호출
비동기 함수
또는 동기 메소드
는 실행중에 간헐적으로 suspended(중지) 될 수 있습니다. 반대로 말하자면, 동기적 함수
는 completion
, error
등을 반환하기전까지 실행됩니다. 즉, 무언가를 하는도중에 동기 함수가 실행되면 동기 함수가 끝나기 전까지 멈추어진것
처럼 보입니다. 그러므로, 비동기 함수
를 사용할 때, async
키워드를 사용하여 비동기적 함수의 특징을 나타낼 수 있는데, throws
를 사용하는 것과 같이 사용하면 됩니다. 반환값이 필요하다면 반환값(->)을 사용하기 전에 async
를 아래와 같이 사용 해주면 됩니다.
이와 같은 비동기적 메소드
를 사용하면 해당 메소드가 값을 리턴할때 까지 실행이 중지되는데, 이를 await
키워드를 사용하면 됩니다. 이는 try
를 사용하는 방식과 비슷합니다. 비동기 메소드 내부에서는 다른 비동기 메소드를 호출할 때만 실행 흐름이 일시 중단됩니다. — 중단될때는 암시적(implicit)
으로 되지않고 꼭 await
키워드를 명시해줍니다.
listPhotos(inGallery:)
함수와 downloadPhoto(named:)
함수는 둘다 네트워크 요청을 필요로 하기때문에 꽤 시간이 걸릴수도있습니다. async
키워드를 통해 사진이 준비되기까지 기다리면 됩니다. 위의 과정은
listPhotos(inGallery:)
함수를 통해 값이 올때까지await
- 위 함수가 suspended(중단) 될 때 다른 프로그램이 실행됩니다. 예를들면, 백그라운드에서 동작하는 새로운 사진 갤러리는 계속 업데이트를 하고있을겁니다. 그 코드는 다음
await
을 실행하거나listPhotos(inGallery:)
함수가 끝날때 까지 계속 실행됩니다. listPhotos(inGallery:)
함수가photoNames
변수에 값을 리턴합니다.sortedNames
과name
변수에는 값이 할당됩니다. 동기적으로 처리가 될거에요.- 다음
await
가 명시되어있는downloadPhoto(named:)
함수가 호출됩니다. 이 코드도 값을 가져와 리턴될때까지 실행이 중단됩니다. downloadPhoto(named:)
함수가 값을 리턴하고, 그 값을photo
할당하고,show(:)
로 인자값을 넘겨줍니다.
await
로 표시되어있는 중단 가능성이 있는 포인트(possible suspension points)는 비동기적 함수 메소드가 리턴할때까지 계속 실행을 중단한다는 것이 중요합니다. 이것을 yielding the thread (스레드 실행을 방지)
라 하는데, await
를사용하면 백단에서 현재 스레드를 중단하고 다른스레드로 넘어가기 때문입니다. 이러한 특징때문에
- 비동기적 함수, 메소드, 프로퍼티
@main
표시가 있는 클래스, 열거형의구조체 Static main()
메소드- 분리된 하위 작업 (
detached child task
)
에서 사용합니다.
Task.sleep(_:) 메소드는 동시성 작업에 대해 배우기에 효과적입니다. 이 메소드는 명시된 시간동안 스레드 사용을 기다리는걸 뜻합니다. 예를 들면,
와 같이 사용할 수 있습니다.
비동기 시퀀스 ( Asynchronous Sequences)
listPhotos(inGallery:)
함수는 모든 배열의 원소들이 준비가 되면 준비완료된 배열을 한번에 리턴합니다. 다른 접근은 비동기 시퀀스
를 실행할 때 컬렉션타입의 원소가 들어올 때마다 각각 배출
하는 것 입니다. 예시는 아래와 같습니다.
For-in
문을 사용하는 대신, for
문을 await
와 함께 사용했습니다. 이와 같이 비동기적 함수나 메소드를 호출할 때, await
구문은 중단 (suspension)이 가능한 구문으로 만들어 줍니다. for-await-in
구문은 반복문에서 각 원소가 들어올 때 중단되었다 다시 실행될 수 있습니다.
Sequence
프로토콜을 준수하여 for-in
문을사용하는것과 같이 for-await-in
문을 AsyncSequence
프로토콜을 준수하여도 사용할 수 있습니다.
병렬시 비동기 함수 호출
await
를 사용하여 비동기 함수를 호출하는것은 한번에 하나의 코드뭉치만 실행합니다. 비동기 코드가 동작할때, 호출자(caller)는 다음 라인으로 넘어가려면 그 코드가 끝날때까지 기다려야합니다. 예를들면, 갤러리에서 처음 번째 사진을 불러오려면, dodwnloadPhoto(named:)
함수에서 세번의 호출을 await
해야합니다
이와같은 접근은 중요한 결점이있는데, 다운로드는 비동기적이고 다른일을 실행시킨다해도, downloadPhoto(named:)
함수는 한번에 한번씩만 실행됩니다. 각각의 사진은 다음 호출이 불리기 전에 온전히 다운로드됩니다. 그러나, 각각의 사진이 개별적으로 다운로드할 필요는 없고, 독립적으로 각각 다운받거나 동시에 다운받으면 됩니다.
위와 같이 병렬적으로 사진을 다운받으려면,
let
상수앞에async
키워드를 다음과 같이 붙혀주면 됩니다.await
키워드를 사용할 키워드 앞에 붙히면 됩니다.
이 예제에서 downloadPhoto(named:)
의 세번의 호출은 그전 호출이 끝날때까지 기다리지 않아도 실행됩니다. 충분한 리소스가 있다면, 동시에 실행될수도 있죠. 각 함수 호출에 await
키워드를 사용하지 않는데 각 함수의 결과값을 기다리며 중단되지 않아도 되기 때문입니다. 대신, 실행은 photos
상수가 정의되기까지 실행이 됩니다. ( await
키워드가 있기때문에 세장의 사진이 다운로드를 완료때까지 다음 실행이 중단됩니다.)
두개의 접근을 정리해보자면,
- 비동기 함수를
await
적으로 호출하면, 함수의 결과를 받고 다음라인의 코드가 실행됩니다. 작업을 순차적으로 진행합니다. - 함수의 결과값이 추후전까지 필요하지않다면
asnyc-let
을 사용하면 됩니다. 이는 작업을 병렬적으로 처리해줍니다. await
과async-let
을 사용하면 자신들을 제외한 다른코드들을 실행해줍니다.await
를 통해 필요한 중단 포인트를 표시하고, 필요하다면 비동기 함수가 리턴될때까지로 시점을 잡을수도 있습니다.
두개의 방식을 각각의 필요에따라 섞어서 사용하면 됩니다.
작업과 작업 그룹
- 작업(task)은 프로그램을 실행하면서 비동기적으로 작업할 수 있습니다. 모든 비동기 코드는 몇몇의 task의 부분으로 실행됩니다.
async-let
구문은 child-task를 만들어줍니다. 또한 task group을 생성하고 그 그룹에 child-task를 추가함으로써 우선순위 및 취소에 대한 추가제어와 task의 수를 동적으로 제어할 수도 있습니다. - Task들은 계층 구조로 배열됩니다. task에 있는 각각의 task는 같은 부모 task를 가지고있고 자식 task을 가지고있습니다. task와 task group간의 명시적관계를
structured concurrency
라고 부릅니다. 이 명시적 부모-자식 관계는 취소전파나 컴파일단에서 에러를 잡아주는 역할을 하기도 합니다.
아래문장은 structured concurrency
개념의 이해를 돕기위해
글을 인용하였습니다.
모든 비동기 함수들은 비동기 Task 일부로 동작하고, 필요에 따라서 하위에 child task를 만들어서 동시에 처리하는 개념입니다.
이 부분에서 child task를 지정하기 위해서 async let 문법이 등장합니다.
Unstructured Concurrency
Structured concurrency
와 동시에 Swift
는unstructured concurrency
도 지원합니다. task group의 task와는 다르게, unstructured task
는 부모 task
가 없습니다. 자유롭게 unstructured task
를 관리할 수 있지만 정확성
에 대해선 책임을 요구
합니다. 현재 actor
에 unstructured task
를 생성하려면, async(priority: operation:)
함수를 호출해야 합니다.
detached task
라 불리는 현재 actor
에 포함되지 않는 unstrucutred task
를 생성하려면, asyncDetached(priority: operation:)
함수를 호출해야합니다.
다음예제는 task
의 결과를 기다리거나 취소하는 작용을 가능하게 해주는 Task.Handle
에 관한 예제입니다.
Task Cancellation
Swift에서 동시성은 cancellation model
과 협력성(cooperative)을 가지고 있습니다. 각각의 task는 실행중에 cancel되었는지 cancellation에 대한 적절한 응답을 해주는지 체크합니다.
CancellationError
와 같은 에러를throw
- 빈 컬렉션이나 nil을 리턴
- 부분적 완료된 작업을 리턴
Cancellation
을 체크하려면
CancellationError
를 throw하는Task.checkCancellation()
을 호출하거나Task.isCancelled
의 값을 체크하고 핸들링
을 하면 됩니다. 예를들면, 갤러리에서 사진을 다운로드하고있는 task
는 다운로드를 부분적으로 delete
하고 네트워크 연결을 close
할 수 있습니다. 취소를 수동으로 하려면 Task.Handle.cancel()
을 호출하면 됩니다.
Actors
참조 타입
(클래스와 동일)한 번에 하나의 작업만 변경 가능
한 상태에 액세스할 수 있도록 허용actor
키워드를 사용
과 같은 특징이 있습니다.
actor
키워드를 사용하면 actor
를 사용할 수 있습니다. TemperatureLogger
라는 actor
는 actor
밖에서 접근할 수 있는 프로퍼티
를 가지고있고 내부접근만 가능한max
프로퍼티가 있습니다.
같은 이니셜라이저 구문을 구조체나 클래스를 이용해 actor
를 인스턴스화 했습니다. actor
의 프로퍼티나 메소드에 접근하려면, await
를 사용해 잠재적 suspension(중단)포인트를 만들어야합니다. 예를들면,
이 예제에서, logger.max
의 접근은 잠재적인 suspension(중단) 포인트를 가지고 있습니다. actor
는 한 번에 하나의 작업만 변경 가능한 상태에 액세스되기 때문에 만약 코드가 다른 task와 interaction 중이라면 이 프로퍼티의 접근에 제한됩니다.
반면에, actor
내에서 actor
의 프로퍼티에 접근하려면 await
를 사용하지 않아도 됩니다. 예를들면, TemperatureLogger
를 새로운 기온으로 업데이트하는 메소드가 있습니다:
update(with:)
메소드는 액터에서 이미 동작하고있고 max
와 같은 프로퍼티를 접근할 때 await
를 사용하지 않습니다. 이 메소드는 왜 actor
가 한 번에 하나의 작업만 변경 가능한 상태에 액세스를 허용하는지에 대한 이유입니다: actor
의 상태에 대한 업데이트는 일시적으로 불변량
을 해소시킵니다. TemperatureLogger
액터는 기온의 리스트 최대 기온 추적하고 새로운 측정이 최대기온이 되면 업데이트를 해줍니다. 업데이트 중에, max
를 업데이트하기 전에 새로운 측정을 업데이트한다면 TemperatureLogger
는 잠시적으로 비일관성적
인 상태가 됩니다. 여러 task를 같은 인스턴스에서 동시 작업하는 것을 방지하는 것은
1. update(with:) 메소드를 호출합니다. 이는 measurements 배열을 업데이트 해 줍니다.
2. 코드가 max를 업데이트하기전에, 기온을 담고있는 배열에서 최대값을 읽어옵니다.
3. max값을 업데이트함으로써 코드실행 종료 합니다.
이와같은 케이스는, 이 코드가 어딘가에서 잘못된 정보를 읽을 수 있는데 이유는 update(with:)
중간에서 갑자기 데이터가 잠시동안 유효하지 않을때 actor
에서 접근이 되어 바뀔 수 있습니다. Swift
의 actor
를 사용하면서 이와 같은 문제를 해결하려면 한번에 하나의 작업만 변경 가능한 상태에서 액세스를 허용하면 되는데 await
가 표시된곳에서 중단(suspension)되어 값을 업데이트 해주면 되기 때문입니다. update(with:)
는 중단(suspension)포인트를 가지고있지 않기때문에, update중간에 어떠한 데이터도 접근이 불가능합니다.
만약 클래스 인스턴스 내부에서와 같이actor
밖에서 프로퍼티에 접근을하고 싶다면 컴파일 에러를 보게 되겠죠.
logger.max
에 await
사용없이 접근하는것은 따로 관리되는 로컬 상태(local state)값에 접근하는것이기 때문에 불가능합니다. Swift
는 actor
의 코드만 local state에 접근 할수 있게끔만을 보장합니다. 이러한 보장은 actor isolation
라 불리웁니다.