Open Jinsujin opened 1 year ago
Swift 비동기 & 동시실행 시작하기
- [ ]
WWDC21
Meet async/await in Swift- [ ]
WWDC21
Discover concurrency in SwiftUI- [ ]
WWDC21
Use async/await with URLSession- [ ]
WWDC22
Swift 의 분산된 Actor 소개- [ ]
WWDC22
Swift Concurrency 시각화 및 최적화- [ ]
WWDC22
Swift Async 알고리즘 소개- [ ]
WWDC21
Swift concurrency: Update a sample app- [ ]
WWDC21
Swift concurrency: Behind the scenes- [ ]
WWDC21
Protect mutable state with Swift actors- [ ]
WWDC21
Explore structured concurrency in Swift- [ ]
WWDC22
Swift Concurrency 를 사용하여 데이터 경합 제거
비동기 함수(asynchronous function)나 메서드(method)는 실행도중에 연기(suspend) 될 수 있는 특별한 종류의 함수나 메서드이다. 이것은 일반적인 비동기 함수나 메서드와 (완료-completion 까지 실행되거나, 에러를 던지거나, return 하지 않는) 차이를 보인다. 비동기 함수나 메서드는 이들 세가지 방법중에 하나를 수행하지만, 어떤것을 기다릴때 중간에 일시중지(pause) 할 수 있다.
비동기 함수나 메서드의 body 안에서, 당신은 연기(suspend) 할 수 있는 각 위치를 표시(mark) 한다.
함수나 메서드가 비동기라는 것을 표시(indicate)하기위해, 당신은 parameter 뒤의 선언에서 async
키워드를 작성한다. 이것은 당신이 throws
를 사용해 함수를 throwing 하는 것을 표시(mark)하는 것과 유사하다.
함수나 메서드가 값을 반환한다면, async
를 화살표(->
) 앞에 작성한다. 예를들어, 갤러리에서 사진의 이름을 가져오는 방법은 아래처럼 한다:
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
비동기나 throwing 을 하는 함수나 메서드의 경우, throws
전에 async
를 작성한다. 비동기 메서드를 호출하면, 메서드가 return 될때까지 실행이 연기(suspend)된다. 호출의 앞에 await
를 작성하면 작업을 연기(suspension)할 수 있는 지점을 표시할 수 있다.
이것은 (error 가 있는 경우에 프로그램의 흐름에 가능한 변경사항을 표시하기 위해) throwing 함수를 호출할때 try
를 작성하는 것과 같다.
비동기 메서드 내부에서, 실행의 흐름은 당신이 다른 비동기 메서드를 호출할때만 연기(suspend)된다.
—연기는 절대 암시적(implicit)이거나 선제적(preemptive)이지 않음—
이는 모든 가능한 연기할 수 있는 지점(suspension point)은 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:)
함수는 모두 네트워크 request 를 만드는 것을 필요로 하기때문에, 완료하는데 비교적 긴 시간이 걸릴 수 있다.
return 화살표 앞에 async
를 작성함으로서 둘다 비동기식으로 만들면, picture 가 준비될때까지 이 코드가 기다리는 동안 앱의 나머지 코드는 계속 실행된다.
위 예제의 동시성 특성을 이해하기 위해, 여기 가능한 실행순서가 있다:
await
까지 실행한다. listPhotos(inGallery:)
함수가 호출되고 해당 함수가 return 할때까지 실행이 연기된다. await
으로 표시된 다음 연기 지점(suspension point)이나, 완료될때까지 실행된다. listPhotos(inGallery:)
가 return 된 후에, 이 코드는 해당 지점부터 실행을 계속한다. 반환되어진 값이 photoNames
에 할당된다. await
표시가 없기때문에, 가능한 연기 지점(suspension point)이 없다.await
표시는 downloadPhoto(named:)
함수 호출이다. 이 코드는 함수가 return 할때까지 다시 실행을 일시정지해서, 다른 concurrent code 를 실행할 수 있는 기회를 준다.downloadPhoto(named:)
return 후에, 해당 반환 값은 photo 에 할당되어진 다음, show(_:)
이 호출되면 argument 로 전달되어 진다.await
으로 표시된 코드에서 가능한 연기지점(suspension point)은 비동기 함수나 메서드가 return 될때까지 기다리는 동안 현재 코드 조각이 실행을 일시중지(pause)할 수 있음을 나타낸다.
이는 yielding the thread (스레드 생성)이라고 불리어진다.
Swift가 뒤에서 현재 스레드에서 코드 실행을 연기(suspend)하고, 대신에 해당 스레드에서 다른 코드를 실행하기 때문이다.
await
가 있는 코드는 실행을 연기(suspend)할 수 있어야 하기때문에, 당신의 프로그램의 특정 위치에서만 비동기 함수나 메소드를 호출할 수 있다.:
@main
으로 표시된 구조체, 클래스, 열거형 의 static main()
메소드에 있는 코드.가능한 연기지점(suspension point) 사이에 위치한 코드는 다른 동시(concurrent) 코드의 중단(interruption) 가능성 없이 순차적으로 실행된다. 예를 들어, 아래 코드는 한 갤러리에서 다른 갤러리로 사진을 이동한다:
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:)
호출 사이에 다른 코드를 실행할 수 있는 방법이 없다.
그 시간동안, 첫번째 photo 가 두 갤러리 사이에 나타나며, 앱의 불변성(invariant) 중 하나를 깨뜨린다.
이 코드 덩어리가 await
하지 않아야 한다는 것을 명확히 하기 위해 나중에 추가하면, 해당 코드를 동기 function 으로 리펙터링 할 수 있다.
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:) 함수가 동기식이면 연기 지점(suspension point)을 포함하지 않는다는 것을 보장한다. 앞으로 이 함수에 동시성(concurrent) 코드를 추가하려고 하면, 연기 시점(suspension point)이 생길 수 있으므로, 버그를 접하는 대신 컴파일타임 에러가 발생한다.
Task.sleep(until:tolerance:clock:) 메서드는 간단한 코드를 작성해서 동시성(concurrency)이 어떻게 동작하는지 배울때 유용하다. 이 메서드는 아무것도 수행하지 않지만, 반환하기 전에 적어도 주어진 나노초만큼 기다린다. 여기 sleep(until:tolerance:clock:) 을 사용해 네트워크 작업 대기를 시뮬레이션하는 listPhotos(inGallery:) 함수가 있다.
func listPhotos(inGallery name: String) async throws -> [String] {
try await Task.sleep(until: .now + .seconds(2), clock: .continuous)
return ["IMG001", "IMG99", "IMG0404"]
}
이전 section에 있는 listPhotos(inGallery:)
함수는 array 의 모든 요소가 준비된 이후에 전체 array를 한번에 비동기적으로 return 한다.
다른 접근방법은 asynchronous sequence 를 사용해 한번에 하나의 컬렉션의 요소를 기다리는 것이다.:
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
일반적인 for-in loop 사용하는것 대신에, 위의 예제에서는 await
를 뒤에 작성한다.
비동기 함수나 메서드를 호출하는 것처럼, await 을 작성하여 가능한 연기 지점(suspension point)을 표시한다. for-await-in loop
는 잠재적으로 각 반복(iteration) 이 시작될때 실행을 연기(suspend) 한다. 이때, 다음 요소를 사용할 수 있기를 기다린다.
Sequence protocol 에 적합성(conformance)을 추가하여 for
-in
loop 에서 자신의 타입을 사용할 수 있는것과 같은 방식으로,
AsyncSequence protocol 에 적합성을 추가하여 for
-await
-in
loop 에서 자신의 유형을 사용할 수 있다.
Concurrency
Swift 는 비동기와(asynchronous) parallel(병렬) 코드를 구조화된 방식으로 작성할 수 있게끔 기본적으로(built-in) 지원한다. 비동기 코드는 일시중지(suspended)나 나중에 실행할 수 있지만, 한번에 프로그램의 한 부분만 실행된다. 프로그램에서 코드를 일시중지나 다시실행하면, (네트워크를 통해 data를 fetching 하거나, 파일을 parsing하는 등)장기적인 작업을 계속 실행 하면서 UI 업데이트와 같은 단기적인 작업을 계속 진행할 수 있다.
프로그램에서 코드를 연기(Suspending)하거나 다시실행(resuming)하면, (네트워크를 통해 data를 fetching 하거나, 파일을 parsing하는 등)장기적인 작업을 계속 실행 하면서 UI 업데이트와 같은 단기적인 작업을 계속 진행할 수 있다.
Parallel code 는 여러개의 코드를 동시에 실행하는 것을 의미한다. — 예를들어, 4코어 프로세서 컴퓨터는 동시에 4개의 코드를 실행할 수 있으며, 각 코어는 task 중 하나를 수행한다. parallel 과 비동기(asynchronous) 코드를 사용하는 프로그램은 한번에 여러개의 작업을 수행한다; 외부 시스템(external system)을 기다리는 작업을 연기(suspend)하고, memory-safe 한 방식으로 코드를 더 쉽게 작성할 수 있다. parallel 이나 비동기(asynchronous) 코드에서의 추가적인 스케쥴링 유연성은 복잡성 증가의 비용이 발생한다. Swift 는 당신의 의도를 표현할 수 있게 하는 방식을 제공한다. 그 방식은 compile-time checking 을 가능하게 한다. 예를들어 actor 를 사용해 mutable state 에 안전하게 접근할 수 있다. 그러나 버그가 많은 코드에 concurrency 를 추가한다고 해서 빠르거나 정확해진다는 보장은 없다. 사실, concurrency 를 추가하는 것은 당신의 코드를 디버깅 하기 더 어려워질 수 있다. concurrency 를 지원하는 Swift language-level 을 사용하면, Swift 가 컴파일 시점에 생긴 문제를 파악하는데 도움이 된다. 이 챕터의 나머지 부분에서는 비동기와 parallel(병렬) 코드의 일반적인 조합을 concurrency 라는 용어를 사용한다.
Note ❗️
당신이 이전에 concurrent 코드를 작성해봤다면, 스레드 작업에 익숙할지도 모른다. concurrency model 은 thread 위에 구축되지만, 당신은 스레드와 직접 상호작용하지 않는다. Swift 에서의 비동기 함수는 실행중인 스레드를 포기할 수 있고, 첫번째 함수가 block 된 동안 다른 비동기 함수가 해당 스레드에서 실행할 수 있다. 비동기 함수가 다시 시작될때(resume), Swift 는 해당 함수가 실행될 스레드에 대해 보증하지 않는다.
Swift 의 언어 지원을 사용하지 않고 concurrent(동시) 코드를 작성하는 것은 가능하지만, 코드는 더 읽기 어려울 수 있다. 예를들어, 아래의 코드는 photo name 리스트를 다운로드 하고, list 의 첫번째 사진을 다운로드 한 다음 사용자에게 해당 사진을 표시한다.
이런 간단한 경우에도, 코드를 completion handler 로 작성해야 하므로, 중첩된 클로저를 쓰게된다. 이런 스타일에서는, 깊은 중첩이 있는 복잡한 코드는 다루기 어려울 수 있다.