study record
[Swift Concurrency] AsyncSequence & Intermediate Task 본문
AsyncSequence 알아가기
AsyncSequence는 비동기적으로 요소들을 생산할 수 있는 sequence를 설명하는 protocol이다.
Swift의 Sequence와 비슷하나, 차이점은 다음 요소를 위해 await 키워드를 붙여야 한다는 것이다.
예제에서의 note
URLSession.data(for:delegate)는 Data를 리턴.
URLSession.AsyncBytes는 URL request로부터 비동기적으로 바이트들을 준다.
HTTP 프로토콜은 서버가 partial requests에 대한 허용성을 지원하는지를 정의하게 한다. 만약 서버가 지원하면, 응답에 대한 byte range를 리턴하는 것을 요청할 수 있다. 한 번에 전체 응답을 받는 것 대신에.
그래서 하나의 파일을 얻고자 요청할 때 그 파일을 전부 받는 것을 기다릴 수도 있지만, 부분적으로 요청을 나누어서 응답을 받을 수도 있다. 그래서 다운로드 작업이 병렬적으로 이루어질 수 있다.
ByteAccumulator(프로젝트에서 정의함)
- sequence로부터 바이트들의 배치를 가져오기 위해
- 파일은 몇백만의 바이트들로 구성된다. 각 바이트를 얻을 때마다 UI를 업데이트하는 것은 시스템을 과부화시키고, 잠재적으로 메인스레드를 블락할 수 있다.
- 따라서 ByteAccumulator가 파일의 컨텐츠를 모으고 각 바이트들의 배치가 가져와진 후에 UI를 업데이트한다. 각 배치를 가져와지면 프로그래스바를 업데이트.
Canceling Tasks
- TaskGroup이나 async let을 사용하면, 시스템이 필요할 때 자동으로 테스크를 취소할 수 있다.
- 더 세밀한 취소 전략을 위해서는 다음의 API들을 사용할 수 있다.
- Task.isCancelled
- 테스크가 살아있으면 true를 리턴
- Task.currentPriority
- Task.cancel()
- 테스크를 취소하고 하위 테스크들도 취소
- Task.checkCancellation()
- 만약 테스크가 취소되고, throw를 던지는게 exit하기 더 쉽다면, CancellationError를 throw
- Task.yield()
- 현재 테스크의 실행을 멈춘다. 시스템으로 하여금 다른 테스크를 더 높은 우선순위로 실행하게 끔하여 자동으로 작업을 취소할 기회를 준다.
- Task.isCancelled
🧪 Task.isCancelled 활용해보기
var downloadTask: Task<Void, Error>?
downloadTask = Task {
do {
let fileData = try await download(file: DownloadFile(name: "graphics-project-ver-2.tiff", size: 30725526, date: Date()))
} catch {
print("error occurred : \\(error.localizedDescription)")
}
}
downloadTask?.cancel()
func download(file: DownloadFile) async throws -> Data {
guard let url = URL(string: "<http://localhost:8080/files/download?\\(file.name)>") else {
throw "Could not create the URL."
}
try Task.checkCancellation()
let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
await addDownload(name: file.name)
await updateDownload(name: file.name, progress: 1.0)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw "The server responded with an error."
}
return data
}
// error occurred : cancelled
func download(file: DownloadFile) async throws -> Data {
guard let url = URL(string: "<http://localhost:8080/files/download?\\(file.name)>") else {
throw "Could not create the URL."
}
print("isCancelled : \\(Task.isCancelled )")
if Task.isCancelled { return Data() }
await addDownload(name: file.name)
print("isCancelled : \\(Task.isCancelled )")
let (data, response) = try await URLSession.shared.data(from: url, delegate: nil)
await updateDownload(name: file.name, progress: 1.0)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw "The server responded with an error."
}
return data
}
// isCancelled : false
// addDownload() runned
// isCancelled : true
// error occurred : cancelled
→ Task.isCancelled를 사용하거나 Task.checkCancellation()을 사용해도 결과는 똑같았다.
@MainActor func addDownload(name: String) async {
print("addDownload() runned")
let downloadInfo = DownloadInfo(id: UUID(), name: name, progress: 0.0)
downloads.append(downloadInfo)
await updateDownload(name: name, progress: 1.0)
}
// isCancelled : false
// addDownload() runned
// updateDownload() runned
// isCancelled : true
// error occurred : cancelled
→ 첫번째 실행되는 메소드에 하위 비동기 메소드를 두었을 때 둘 다 실행되는 것을 확인.
→ .cancel()을 하더라도 처음 실행되는 메소드에서는 child 메소드까지 다 실행되는 듯..?
@MainActor func addDownload(name: String) async {
print("addDownload() runned")
let downloadInfo = DownloadInfo(id: UUID(), name: name, progress: 0.0)
downloads.append(downloadInfo)
print("Task.isCancelled : \\(Task.isCancelled)")
if Task.isCancelled { return }
await updateDownload(name: name, progress: 1.0)
}
// isCancelled : false
// addDownload() runned
// Task.isCancelled : true
// isCancelled : true
// error occurred : cancelled
→ 그런 줄 알았으나 중간에 Task.isCancelled를 확인하는 처리문을 두면 실행되지 않는다.
→ Task.isCancelled 조건문이 필요한 것으로 보인다.
Manually Canceling Tasks
.task() view modifier는 자동으로 뷰가 사라질 때 취소를 해준다. 그러나 이 .task() 안에 없는 작업은 비동기 작업에 대해 취소를 하지 않고 있다.
Storing State in Tasks
각 테스크는 또다른 테스크를 호출할 수 있다. 여기에서 공유 자원을 독립시키는 것은 어려울 수 있다.
→ task-local 프로퍼티를 줌으로써 이를 해결하고자 한다. 이는 객체가 즉각적인 View에 이용가능할 뿐만 아니라 하위 뷰들에도 이용이 가능하다.
task-local 값을 바인딩하는 것은 즉각적인 작업 뿐만 아니라 하위 작업에도 이용이 가능한 것이다.
너무 많은 task-local storage 프로퍼티를 사용하는 것은 각 바인딩을 위해 클로저를 사용하게 되어 이해하기 어렵게 될 수 있다. 그래서 적은 값들의 바인딩에 쓰는 것이 좋다. 전체 data model이나 configuration 객체.
TaskLocal → static property의 값이 오직 주어진 task의 범주 안에서만 할당 되는 것을 보장한다.
전역 변수를 활용할 수 있는 범주 제공.
🧪 task-local 활용해보기
enum Transaction {
@TaskLocal static var id: UUID? = nil
}
// id 할당하는 방법
await Transaction.$id.withValue(UUID()) {
print(Transaction.id)
}
TaskLocal은 isCancelled 프로퍼티와 비슷하게 scope를 만든다. 따라서 이게 값이 정의된 범주에 따라서 다르게 적용된다. 값을 호출한 그 context를 보고 어떤 값이 적용되는지가 나뉜다.
await Transaction.$id.withValue(UUID()) {
print("1 \\(Transaction.id)") // original value
await Transaction.$id.withValue(UUID()) {
print("2 \\(Transaction.id)") // new value
}
print("3 \\(Transaction.id)") // original value
}
// 1 Optional(D368A683-29A0-4892-BC72-AE12E206D495)
// 2 Optional(79A0886A-6149-413A-86E3-4D64D7006340)
// 3 Optional(D368A683-29A0-4892-BC72-AE12E206D495)
// 1, 3 같은 값이 적용되는 것을 알 수 있다.
1, 3 같은 값이 적용되는 것을 알 수 있다.
await Transaction.$id.withValue(UUID()) {
print(Transaction.id) // assiged value
Task.detached {
print(Transaction.id) // nil
}
}
// 새 context이므로 적용 안 됨
await Transaction.$id.withValue(UUID()) {
let transaction = Transaction.id
Task {
print(transaction) // the task local UUID
}
}
// 복사해서 사용해야 한다.
await Transaction.$id.withValue(UUID()) {
let transaction = Transaction.id
Task {
await Transaction.$id.withValue(transaction) {
print(Transaction.id) // the task local UUID from the outer scope
}
}
}
outer scope의 Task Local 값을 활용하고자 한다면 그 값을 복사해두고 사용하는 방법이 있다.
새 context에는 그 값이 적용되지 않기 때문이다.