async/await/MainActor/Task
async와 await는 Swift에서 비동기 처리를 위한 키워드이다.
async는 비동기 작업을 수행하는 함수나 메서드를 선언할 때 사용되고,
await는 비동기 작업의 완료를 기다리고, 결과를 반환받을 때 사용된다.
*아래 설명은 함수/메서드 구분하지 않음. 함수로 통일함
async 키워드는 비동기적으로 실행되는 코드 블럭을 표시한다.
비동기 함수는 작업을 시작하고 중간에 일시 중지되거나, 다른 작업을 수행하는 동안 '제어를 반환할 수 있다.'
-> 스레드가 차단되지 않고 동시에 여러 작업을 수행할 수 있다.
async 함수 내부에서 await 키워드를 만나면 비동기 작업이 완료될 때까지 비동기 함수의 실행이 일시 중단된다.
조금 더 정확히 말하면 await 키워드를 만나면 현재 실행 흐름을 '일시 중단'한다. (suspend)
현재 실행 흐름을 일시 중단한다는게 어떤 의미일까?
일반적으로 프로그램은 한 줄씩 순차적으로 진행된다.(위에서 아래로 순차)
하지만 비동기 처리는 특정 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행할 수 있어야 한다.
await 키워드를 만나면 현재 비동기 함수의 실행 흐름이 잠시 중단되며, 중단된 상태에서 다른 작업이 수행되고
나중에 비동기 작업이 완료되면 실행 흐름이 다시 재개된다.
즉 일시 중단은 해당 비동기 작업이 완료될 때까지 현재 코드 블록에서 실행을 멈추는 것을 의미한다.
(쉽게 말하면 await 아래 코드들은 비동기 작업이 끝나고 나서 실행된다는 의미)
⭐️그리고 현재 코드에서 비동기 작업의 결과를 기다리는 동안, 다른 작업을 수행할 수 있다!
즉 스레드를 차단하지 않는다는 얘기! 이것이 await의 핵심이다.
다시, await 키워드를 만나면 비동기 작업이 완료될 때까지 현재 코드의 실행이 잠시 중단되지만,
스레드가 차단되는 것이 아니기 때문에 다른 작업을 동시에 할 수 있다.
(ex. 시스템에 제어권을 넘겨서 관련 작업을 한다든가, 다른 스레드의 작업도 계속 진행 할 수 있다.)
이러한 방식을 통해서 여러 비동기 작업을 병렬로 처리하거나,
'다른 작업을 수행하면서도 비동기 작업의 완료를 기다릴 수 있다.'
즉 오매불망 결과만 기다리지 말고, 기다리는 동안 다른 일을 하고 있어라는 말 ㅎㅎ
그러면 async 메서드는 항상 백그라운드 스레드에서 실행될까? 🤔
놉,
async 키워드는 해당 함수/메서드가 '비동기적으로 실행될 수 있음'을 나타내는 것이다.
어떤 스레드에서 실행되는지를 명시적으로 결정하지는 않는다는 말.
async 메서드는 호출자가 있는 스레드에서 시작되고, 그 이후에는 async 메서드 내부의 코드 흐름에 따라
다른 스레드에서 실행될 수도 있다.
일반적으로 async 메서드는 호출한 스레드에서 시작되고, 비동기작업이 시작/완료되는 시점에 다른 스레드로 전환되기도 한다.
import SwiftUI
class AsyncAwaitViewModel: ObservableObject {
@Published var dataArray: [String] = []
func addAuthor1() async {
let author1 = "Author1 : \(Thread.current)"
self.dataArray.append(author1)
try? await Task.sleep(nanoseconds: 2_000_000_000)
//백그라운드 스레드에서 아래 작업이 이루어짐
let author2 = "Author2 : \(Thread.current)"
await MainActor.run(body: {
self.dataArray.append(author2)
let author3 = "Author3 : \(Thread.current)"
self.dataArray.append(author3)
})
}
func addSomething() async {
try? await Task.sleep(nanoseconds: 2_000_000_000)
let something1 = "Something1 : \(Thread.current)"
await MainActor.run(body:{
self.dataArray.append(something1)
let something2 = "Something2 : \(Thread.current)"
self.dataArray.append(something2)
})
}
/*
async 함수는 호출자의 스레드에서 실행을 시작하지만,
함수 내에서 await을 만나면 현재 작업을 일시 중단하고 이벤트 루프로 제어를 반환함(다른거 하고 있어)
해당 비동기 작업이 완료되면 콜백 또는 핸들러를 통해 작업을 재개함
*/
}
먼저 Task.sleep을 살펴보자.
Task.sleep은 async throws다. 즉 비동기 메서드이고 에러를 던질 수 있다는 말.
Task.sleep은 비동기적인 대기 시간을 생성해서 현재 실행 스레드를 잠시 중단한다. (근데 왜 nanoseconds일까..)
특정 시간 동안 현재 스레드를 차단하지 않고, 다른 작업이나 스레드가 실행될 수 있도록 한다.
기본적으로 백그라운드에서 비동기적으로 실행된다.
그런데 위 코드에서 경고문이 하나 뜨는데..
현재 스레드를 확인하기 위해서 사용했는데 이런 에러가 떠서 스택오버플로우를 찾아봤다.
아마 앞으로는 이 방식 자체가 에러로 바뀔 거 같은데, async/await를 사용할 때 메인스레드에서 실행중인지 여부를 확인하기 위해
Thread.isMainThread 등을 사용해도 같은 경고가 발생한다. 아마 내부적으로 알아서 스레드 관리가 이루어지기 때문에
굳이 확인하지 않아도 된다는 늬앙스 같다 ㅎ..우선은 그냥 확인용으로 동작은 하니까 넘어가자.
MainActor는 뭘까?
먼저 액터가 뭐지?
액터는 동시성 프로그래밍에서 사용되는 '실행 단위'이다.
액터는 여러 스레드에서 안전하게 동작할 수 있는 독립적인 실행 컨텍스트로 구성되고, 각 액터는 자체적인 상태를 가지고 있으며
다른 액터와는 분리되어 실행된다.
-> 여러 스레드에서 안전하게 동작하고 상태를 관리할 수 있으며, 동시성 관련 문제를 간편하게 다루기 위해 도입된 개념'정도로 이해하고 넘어간다.
MainActor
MainActor는 특정 액터가 '메인 스레드에서 실행되어야 함'을 나타낸다.
메인 스레드의 역할은? 일반적으로 UI 업데이트와 관련된 작업을 처리한다.
즉 메인 스레드에서 실행되어야만 하는 코드를 구분하기 위해 MainActor를 사용할 수 있다.
MainActor.run은 비동기 클로저를 통해서 메인스레드에서 실행된다.
-> 메인 스레드에서 실행되어야 하는 작업을 명시적으로 표시하고, UI 업데이트와 관련된 코드를 안전하게 실행할 수 있음.
뷰에서 확인해보기
struct AsyncAwaitPrac: View {
@StateObject private var vm = AsyncAwaitViewModel()
var body: some View {
List {
ForEach(vm.dataArray, id: \.self) { data in
Text(data)
}
}
.onAppear {
Task {
await vm.addAuthor1()
await vm.addSomething()
let finalText = "Final Text: \(Thread.current)"
vm.dataArray.append(finalText)
}
}
}
}
Task??
Task는 Swift에서 동시성 작업을 처리하기 위한 도구로, 비동기 코드의 실행을 관리하고 조절하는 데 사용된다.
Task를 사용해서 비동기 작업을 생성하고 실행하면, 해당 작업은 별도의 실행 컨텍스트에서 동작한다.
* 실행 컨텍스트란?
Execution Context - 프로그램의 실행 중에 현재 상태와 실행 환경을 저장하고 유지하는데 사용되는 개념.
실행 컨텍스트는 코드의 실행에 필요한 정보,데이터를 보유하고, 실행 중인 코드의 상태를 추적하고 관리함.
일반적으로 비동기 작업을 병렬로 실행하거나 우선순위를 지정해야 하면 Task를 사용한다.
또한 비동기 작업을 취소하거나 대기 시간을 설정하는 등의 작업도 Task를 통해 수행한다.
(async 메서드를 꼭! Task 내부에서 실행해야 하는 것은 아니지만, 대부분 Task 내부에서 수행하고, 관련 편의 기능이 많음)
먼저 Task 부분을 보면
addAuthor1과 addSomething이 둘 다 async 메서드이기 때문에 앞에 await가 붙어있다.
Task 내부에 작성된 비동기 코드는 순차적으로 실행되고, await이 붙어있기 때문에
addAuthor1() 메서드가 완료될 때까지 기다린 후에, 다음 줄인 addSomething() 메서드가 실행된다.
그리고 나서 finalText가 생성되고 vm.dataArray에 추가된다.
addAuthor1
먼저 author1의 스레드는 어떤 스레드인지 모른다. (내부적으로 알아서 결정됨_메인or 백그라운드)
그다음 Task.sleep을 통해서 백그라운드 스레드로 전환이 되기 때문에
author2의 스레드는 백그라운드 스레드가 된다.
그 다음 메인액터를 통해 메인스레드로 전환되기 때문에 author3의 스레드는 메인스레드가 된다.
addSomething
Task.sleep으로 시작하기 때문에 something1의 스레드는 백그라운드 스레드이다.
그 다음 메인액터를 통해 메인스레드로 전환되기 때문에
something2의 스레드는 메인스레드가 된다.
Task 내부의 실행 순서까지 한꺼번에 보면
1. addAuthor1가 실행된다.
author1가 데이터 어레이에 추가된다.
2. 2초 기다린다(백그라운드 스레드로 전환)
3. author2는 백그라운드 스레드에서 생성된다.
4. 메인액터를 통해 author2가 데이터 어레이에 추가되고, author3가 생성되고 어레이에 추가된다.
-> Author1이 뷰에 표시되고, 2초 뒤에 Author2와 Author3이 뷰에 표시된다.
-----이 작업이 다 끝나고 나서 addSomething이 실행된다.
1. something1이 백그라운드에서 생성된다.
2. 메인액터를 통해 메인스레드로 전환되고, something1이 어레이에 추가된다
그리고 something2가 생성되고 어레이에 추가된다.
----이 작업이 다 끝나고 나서 finalText가 생성된다. 현재 스레드는 위에서 메인이었기에 메인에서 생성된다.
그리고 나서 데이터 어레이에 finalText가 추가된다.
총 순서는
Author1이 표시됨 -> 2초 뒤 -> Author2/Author3이 표시됨 -> 2초 뒤 -> Something1, Something2, FinalText가 순차적으로 표시.
처음에는 꽤 이해하기 어려운 개념이었는데, 관련 자료를 찾아 읽어보고 계속 보다보니까 조금씩 이해가 되고 있다.
물론 많이 써보는게 가장 확실한 방법이겠지 ㅎㅎ..
Task 관련해서는 우선순위 설정, 작업 중간 취소, onAppear 내부에서 Task 생성 대신 더 효율적인 방법 등
알아봐야 할 부분이 많기 때문에 다음 포스팅에서 다루도록 하겠다!
'iOS Developer > Concurrency' 카테고리의 다른 글
4. async let (0) | 2023.07.12 |
---|---|
3. Task/ .task / cancel() / priority / detached (0) | 2023.07.12 |
1. 비동기 처리 방법/ Completion/ Combine / async/await (0) | 2023.07.10 |
0. Do - Catch / Try / Throw 에러처리 (0) | 2023.07.05 |
DispatchQueue / GCD / multi threading / 스레드 / 비동기 (0) | 2023.05.26 |