일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- ios
- 애니메이션
- SWIFT
- 리펙터링
- Refactoring
- ribs
- RxCocoa
- Clean Code
- HIG
- uiscrollview
- 리펙토링
- UICollectionView
- 스위프트
- swift documentation
- Xcode
- swiftUI
- combine
- tableView
- 리팩토링
- Observable
- collectionview
- map
- Protocol
- Human interface guide
- rxswift
- 클린 코드
- MVVM
- clean architecture
- uitableview
- UITextView
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - Swift] 1. Swift Concurrency 개념 - async, await, Task, async let, Actor, @MainActor, deadlock, nonisolated, DispatchQueue와 차이점 본문
[iOS - Swift] 1. Swift Concurrency 개념 - async, await, Task, async let, Actor, @MainActor, deadlock, nonisolated, DispatchQueue와 차이점
jake-kim 2022. 11. 29. 22:271. Swift Concurrency 개념 - async, await, Task, async let, Actor
2. Swift Concurrency 개념 - async, await를 이용하여 api 호출해보기
async, await 란?
- iOS13+ 사용 가능
- 비동기 처리를 위해서 completion closure를 사용하지 않고 async, await 키워드를 사용하여 더욱 가독성 있는 비동기 프로그래밍이 가능한 것
* 구체적인 async, await 내용은 이전 포스팅 글 async, await 사용 방법 참고
- async 키워드) 함수 정의 부분, throws 앞에 사용
func fetchItems() async {
}
func fetchSomeItems() async throws -> String {
}
- await 키워드) 사용하는 쪽에서 호출
await fetchItems()
await fetchSomeItems()
Task 개념
- Task는 async 작업의 단위이며, 스위프트에서 DispatchQueue나 OperationQueue, Thread 따로 없이 작성하면 main thread 인 sync로 동작 하는데, 이때 Task 블록으로 감싸서 async 코드를 수행할 수 있도록 제공
@frozen struct Task<Success, Failure> where Success : Sendable, Failure : Error
ex) sync에서 async 호출을 Task없이 async 함수 호출 시 에러 발생
- await를 붙여도 에러 발생)
- Task 블록으로 감싸게 되면 해결
Task {
await fetchItems()
}
- 만약 Task 블록 없이 async 함수를 호출하려면, async 함수를 호출하는 쪽도 async여야 가능
- viewDidLoad()는 override 함수라, async를 붙이지 못하지만 이론상 async를 붙이면 Task 블록 없이도 호출 가능
override func viewDidLoad() async { // <-
super.viewDidLoad()
await fetchItems()
}
cf) 기능은 DispatchQueue.global().async로 호출하는 부분과 유사
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async {
self.fetchHeavyItems()
}
}
func fetchHeavyItems() {
// get some heavy items
}
}
ex) await 실행 순서
override func viewDidLoad() {
super.viewDidLoad()
print("will enter task block")
Task {
print("did enter task block")
try? await Task.sleep(for: .seconds(10)) // await를 만남 -> 다음 라인의 코드 실행시키지 않고 대기
print("will out task block")
}
print("some another code")
}
/*
will enter task block
some another code
did enter task block
will out task block
*/
Task와 DispatchQueue 스레드 차이
- 공통점
- DispatchQueue 블록 안에 들어가면 임의의 스레드로 처리
- Task 블록 안에서도 임의의 스레드로 처리
- 차이점
- Task 블록 안에서 코드가 대기하여 다음 코드를 실행하지는 않지만, 내부적으로 스레드는 block 되지 않고 다른 일을 처리할 수 있도록 효율적으로 설계
override func viewDidLoad() {
super.viewDidLoad()
Task {
try? await Task.sleep(for: .seconds(10)) // await를 만남 -> 다음 라인의 코드 실행시키지 않고 대기
}
}
- DispatchQueue는 해당 Thread가 점유하고 일을 하고 있으면, 그 스레드는 block되어 해당 일을 끝낼때까지 다른 일 처리를 못하는 상태
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async {
self.fetchHeavyItems()
}
}
func fetchHeavyItems() {
// get some heavy items
}
async let 사용 방법 (Concurrency)
- await로 데이터를 얻지 않고, async로 프로퍼티를 선언하여, 동시성 처리에 사용
async let a = getA() // async let 선언 방법
ex) 예제로 사용할 async 메소드 정의
func getA() async -> Int {
try? await Task.sleep(for: .seconds(3))
return 3
}
func getB() async -> Int {
try? await Task.sleep(for: .seconds(5))
return 5
}
- async let 없이 호출할 경우 - 두 개 모두 동시에해도 되는 일이지만, 3초 대기 후 5초 대기 하여 총 8초간 대기하는 비효율적인 코드
// 8초 소요
Task {
let a = await getA()
let b = await getB()
print(a + b)
}
- async let을 사용하여 동시성 프로그래밍
// 5초 소요
Task {
async let a = getA()
async let b = getB()
let sumAB = await (a + b) // <- 비동기 바인딩
print(sumAB)
}
Actor 개념
- 동시성 프로그래밍에서 가장 주의해야할 것인 Deadlock, race condition인데, 이 문제를 해결하기 위해 Swift에서 만들어낸 자료형
- class와 같이 reference type이고 actor는 한 번에 하나의 작업만 액터의 상태를 변경할 수 있는 형태
- mutex 역할을 내부에서 자동으로 처리
- 사용 방법은 class로 정의하고, 초기화해서 사용하는 것과 동일
// actor 사용 방법
actor Counter {
private let name: String
private var count = 0
init(name: String) {
self.name = name
}
func plus() {
count += 1
}
}
let counter = Counter(name: "counter")
// 코드 참고: https://swiftsenpai.com/swift/swift-concurrency-get-started/
* Deadlock: 둘 이상의 프로세스가 다른 프로세스가 점유하고 있는 자원을 서로 기다릴 때 무한 대기에 빠지는 상황
(4가지 조건이 모두 충족되면 Deadlock 발생)
- mutual exclusion - 자원 하나는 한 프로세스만 사용 가능
- hold and wait - 하나 이상의 자원을 점유하면서 다른 프로세스가 점유하고 있는 자원을 대기하는 상태
- no preemtion - 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 뺏을수 없음
- circular wait - 자원을 요구하며 대기하는게 순환적인 형태
Actor 사용 방법
- actor 정의
actor Counter {
private name: String
private var count = 0
init(name: String) {
self.name = name
}
func plus() {
count += 1
}
}
- 일반 인스턴스 메소드를 호출하듯이 actor의 메소드 호출은 불가능
- 동시에 호출하는 멀티 스레드로부터 counter.plus()가 호출될 수 있으므로, race condition을 사전에 컴파일 에러로 막기 위해서 swift에서 에러 발생
- actor의 메소드를 사용할땐 Task, await 키워드를 사용
let counter = Counter(name: "counter")
Task {
await counter.plus()
}
nonisolated 키워드
- let키워드로 선언하여 읽기만 가능한 actor는 race condition이 발생하지 않는것을 보장하므로, 메소드 앞에 nonisolated를 붙여서 사용하는쪽에서 Task, await 키워드 없이 접근이 가능하게 제공
// nonisolated 키워드 예시
actor Counter {
private let name: String
private var count = 0
init(name: String) {
self.name = name
}
func plus() {
count += 1
}
nonisolated func getName() -> String { // <-
name
}
}
// 사용하는쪽
let counter = Counter(name: "counter")
counter.getName() // Task, await 키워드 없이 접근 가능
@MainActor 키워드
- 항상 main에서 동작하는 actor를 의미
- 항상 메인 스레드에서 실행되어야 하는 클래스에다 @MainActor 키워드를 사용하여 구현
@MainActor
class SomeClass {
}
- 특정 메소드만 메인 스레드에서 항상 실행되어야 하는 경우, 메소드 위에 @MainActor 키워드 사용
class SomeClass {
@MainActor
func runSomeMethod() {
}
}
- cf) Swift 5.5에서는 UIKit, SwiftUI 구성 요소가 모두 MainActor이므로, 백그라운드 작업이 완료된 후 UI를 업데이트 하려고 할 때 DispatchQueue.main.async {}로 수동 업데이트 필요 없이 자동으로 메인에서 동작
* 전체 코드: https://github.com/JK0369/ExAsyncAwaitConcurrency
* 참고
https://developer.apple.com/documentation/swift/mainactor
https://developer.apple.com/documentation/swift/actor
https://developer.apple.com/documentation/swift/task
https://developer.apple.com/tutorials/app-dev-training/modernizing-asynchronous-code
https://swiftsenpai.com/swift/swift-concurrency-get-started/