본문 바로가기

프로그래밍/iOS,macOS

[concurrency] swift async/await

다른 언어를 사용하다가 ios쪽 개발을 할때 코루틴과 같은 시퀀셜한 비동기 처리가 없는점이 매번 아쉬웠는데.. swift 5.5 부터 async await 가 지원되기 시작했다. 당연하게도 애플쪽 하위호환이란건 바라지도 말아야 하는지라...
그나마 xcode 13.2 부터 사용 가능하니 내부 샘플 프로젝트등에 적용해 보려고 대강 내용만 정리해 본다.
ios15 이하를 타겟으로 하는 경우 import _Concurrency 와 같이 비공식적으로 사용해야 한다. (이마저 없었으면 한 2년 뒤에나 사용할 듯)

async

@MainActor
func getSomething() async -> [Int] {
}

비동기 메쏘드는 메쏘드명 다음에 async 라는 키워드로 정의된다.
메인 쓰레드에서 동작해야 하는 경우 @MainActor 어노테이션으로 해당 메쏘드가 메인 쓰레드에서 동작하도록 지정할 수 있다.

 

await

@MainActor
func getSomething() async -> [Int] {
    // 별도의 비동기 작업
    let items = await fetchData()
    return items
}

await 가 호출되면 비동기 처리가 완료될때까지 실행이 멈추게 되는데(suspend), 이때 시스템은 해당 쓰레드를 다른 작업에 사용하게 된다. await 를 사용하기 위해서는 메쏘드 혹은 블럭이 async 로 지정되어야 하므로, 보통 async 메쏘드 내부에서만 사용하게 된다.
async 메쏘드가 아닌 일반 메쏘드에서 호출하는 경우 별도의 async를 구성하는 객체를 정의하거나, 제공되는 Task 를 사용한다.
(swiftUI 에서는 .task{ } 를 사용)

func doWork() {
   Task {
       let result = await getSomething()
       update(result)
   }
   
   // deprecated async { }
   // async {
   //     await getSomething()
   // }
}

 

async 메쏘드가 아닌 곳에서 await function() 을 호출하면 'async call in a function that does not support concurrency' ,
combine에서는 'Cannot pass of type (Output) async  to parameter expecting synchronous function type' 과 같은 에러가 발생한다.

 

 

기존 콜백 방식을 async 형식으로 호출

대부분 주변의 메쏘드들은 여전히 콜백을 사용하니 콜백 메쏘드들 async 로 변환해 주어야 한다.
withCheckedContinuation, withCheckedThrowingContinuation 을 사용해 기존 콜백의 결과를 async 로 리턴할 수 있다.

func getSomething() async -> [Int] {
    await withCheckedContinuation { continuation in
         doSomethingWithCallback(completion: continuation.resume(returning:))
    }
}

continuation.resume(returning:)
continuation.resume(throwing:)
continuation.resume(with: Result)

func getSomething() async throws -> [Int] {
    await withCheckedContinuation { continuation in
    	DispatchQueue.main.async {
             do {
                 // 비동기 작업
             } catch {
                 // 에러인 경우
                 continuation.resume(thorwing: Error)
                 return
             }
             continuation.resume(returning: [])
        }
    }
}

 

 

여러 작업의 동시 실행

async 작업을 병렬로 동시에 수행한다. async let 을 통해 작업을 선언하고, await를 어떤 타입으로 결과를 받을지 지정한다. 작업들이 완료되면 await 뒤에 정의된 데이터 타입으로 return 된다.  

func doSomething() async {
	async let job1 = work1()
	async let job2 = work2()

	let results = await [job1, job2]
}

 

TaskGroup 을 사용한 병렬 처리

기본적인 사용방법은 그룹에 task 를 추가하고, for await in 으로 결과를 가져와 리턴한다.
TaskGroup은 next(), waitForAll(), map, compactMap 같은 컬렉션 메쏘드들을 제공하므로, 태스크들을 선택해 리턴할 수 있다.

await withTaskGroup(of: ChildTaskResult.Type, returning: GroupResult.Type) { taskGroup in
    // 리턴할 데이터
    var result: GroupResult.Type
    
    
    // 그룹에 각 태스크 추가
    taskGroup.addTask {
    	return await taskFunc()
    }
    
    // 각 결과 확인
    for await taskResult in taskGroup {
    	// GroupResult.Type으로 리턴할 데이터 처리
        
    }
    

    // GroupResult.Type 리턴
    return result
}

 

 

 

 URLSession

func fetchImage(url: URL) async throws -> UIImage {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw specificError
    }
    
    guard let image = UIImage(data: data) else {
        throw specificError
    }
    
    return image
}