[iOS] About Swift Concurrency
드디어..!
Swift Concurrency에 대해 글을 쓰는 날이 왔습니다..!
코드적으로 사용하는 것은 어렵지 않았으나 그 외적인 내용이 굉장히 많더군요..!
1. Swift Concurrency 도입 배경
기존 비동기 프로그래밍 - GCD 에서의 문제점
콜백지옥(callback hell)
기존의 비동기 함수 정의 방식
- 오래 걸려서 얻는 결과값을 콜백 클로저 형태로 돌려받음
// 콜백 함수 형태로 사용
func getImage(completionHandler: @escaping (UIImage?) -> Void) {
DispatchQueue.global().async {
sleep(5)
let image = UIImage(systemName: "person")
completionHandler(image)
}
}
- 위의 코드를 무한히 호출하게 되면 엄청난 중첩코드가 발생 >> 유지보수 관점에서 좋지 않음
코드의 복잡성
Race Condition이나 메모리 누수와 같은 동시성 관련 버그 발생 가능
2. Swift Concurrency 의 장점
코드 가독성 향상
- async/await 로 비동기 작업을 동기 코드처럼 직과적으로 작성 가능
안정성 강화
- Acotr로 데이터 경쟁 문제 방지
- Structured Concurrency로 작업의 생명 주기를 명확히 관리
성능최적화
- 작업 스케줄리과 스레드 풀을 통해 시스템 리소스 효율적으로 사용
에러 처리 간소화
- try/await 를 통해 에러 처리 명확하게 명시
기존 비동기 코드와 호환
- Continuation을 사용해 클로저 기반 API를 async/await로 변환
3. async/await
async
- 함수가 비동기 작업을 수행함을 나타냄
- 하나의 함수 자체가 실행 / 중간에 멈춤 / 재개 되는 것이 가능한 함수 >> 새로운 방식의 비동기 처리/함수
- 콜백방식이 아닌 값을 직접 리턴하는 것도 가능
await
- 비동기 작업의 완료를 기다림
- 잠수 멈출 수 있는 중간 포인트 개념 - 스레드 yielding 이라고도 불림 > 스레드 제어권을 운영체제에 넘김
- 비동기 함수를 호출하는 것이 새로운 작업을 생성하는 건 X
// 비동기 함수 정의
func asyncMethod() async -> String {
// 비동기 작업
return result
}
// 비동기 함수의 실행
Task {
let result = await asyncMethod()
}
4. Task 와 Structured Concurrency
Task
- 비동기처리를 할 수 있는 하나의 작업 단위
- 생성 후, 즉시 비동기 처리 시작
- 비동기 함수를 Task 내부에서만 호출 가능 >> 비동기 실행 컨텍스트(비동기함수가 실행될 수 있는 실행환경)를 만드는 것
- 각 Task는 실행 중인 상태와 데이터를 안전하게 과리, 서로 다른 작업간에는 상태 공유 X
- 작업 내부의 코드는 순차적으로 동작
- 컴파일 시점에 thread-safe 하지 않은 코드 작성 불가능 하도록 함 >> Race Condition 문제 방지
- 다른 Task와는 병렬적으로 실행, 런타임에서 적절한 스레드에 작업을 분해하여 실행
5. 작업 스케줄링
스레드 풀을 통해 작업을 스레드에 효율적으로 분배
스레드 재사용
- await로 작업이 중단되면 스레드를 다른 작업에 할당
- await 이후 작업이 다른 스레드에서 재개될 수 있음
스레드 풀 관리
- CPU 코어 수와 시스템 부하를 고려하의 최적의 스레드만 생성
성능 최적화
- 불필요한 컨텍스트 스위칭 최소화
GCD
- 고정된 스레드 풀 >> 작업 수 증가 > Context Switching 많아짐
- 작업의 중단 및 재개 시, 작업이 실행 중이던 스레드를 해제하지 않고 대기
- 이는 한정된 스레드 자원 내 불필요한 스레드 대기와 스위칭 유발
- 결국 개발자가 스레드와 작업간의 효율적 매핑을 해야함
Swift Concurrency
- 스레드 재활용 >> await로 중단되면 현재 스레드는 즉시 해제되어 다른 작업 처리 가능
- 중단 시, 다른 작업이 처리 가능 하기 때문에 컨텍스트 스위칭 최소화, 불필요한 대기 스레드 역시 치소화
- 적절한 스레드 관리 개발자가 XX
- Task의 우선순위를 기반으로, 중요한 작업이 자동으로 먼저 실행되도록 관리
6. Actor
- Race Condition을 방지하기 위해 설계된 동시성 모델
- Acotr 내부의 상태는 단일 작업만 접근할 수 있도록 보장
사용 예시
actor SharedResource {
private var data: [String] = []
func addItem(_ item: String) {
data.append(item)
}
func getAllItems() -> [String] {
return data
}
}
let resource = SharedResource()
Task {
await resource.addItem("First")
}
Task {
await resource.addItem("Second")
}
Task {
print(await resource.getAllItems()) // ["First", "Second"]
}
- actor를 활용하여 SharedResource를 Race Condition이 안 일어나도록 정의
Race condition 발생 예제
var sharedValue = 0
func incrementSharedValue() {
sharedValue += 1
}
Task {
incrementSharedValue()
}
Task {
incrementSharedValue()
}
print(sharedValue) // 데이터 충돌로 인해 예측 불가능한 값 출력 가능
- 기대 결과값은 2 이지만, 2 외에 다른 값이 나올 수 있음
고로 다시 정리해보면
- 공유 상태를 안전하게 관리
- 동시성 문제가 발생 가능성이 있을 때
- 데이터 무결성 유지
위의 조건이 필요할 때 사용
하지만 반드시 사용해야하는 것은 아님
7. Continuation
- 기존의 클로저 기반 비동기 API를 async/await로 변환하는 API
CheckedContinuation
- 컴파일러가 작업 완료 여부를 확인
- 일반적으로 사용
UnsafeContinuation:
- 개발자가 작업 완료를 보장해야 하며, 컴파일러가 확인하지 않음
- 성능 최적화가 필요한 특수한 상황에 사용
왜 변환?
기존의 방식은 태스크마다 스레드를 새로 생성해서 비동기 처리
but Continuation을 통해 async/await 형식으로 바꾸면
스레드를 재활용할 수 있게 코드를 바꿔주는 것!
func fetchData() async throws -> String {
try await withCheckedContinuation { continuation in
fetchData { data, error in
if let data = data {
continuation.resume(returning: data)
} else if let error = error {
continuation.resume(throwing: error)
}
}
}
}
사용법은 쉬운데..
참 오랫동안 이해가 안돼서 미루고 미루웠던.. Swift Concurrency 였습니다..
드디어 코드적으로도 개념적으로도 이해가 되는 날이..! 왔네요 ㅎ
.
.
.
.
