iOS/Swift

[Swift] async(비동기) await & concurrency(동시성)

융식 2023. 3. 14. 16:03

sync (동기) 

작업을 순차적으로 실행하게됩니다.

우리가 평소에 실행하는 코드를 예를 들면

print("1")
print("2")
print("3")

위에서 부터 차례대로 1 2 3이 실행되게 됩니다.

 

근데 저런식으로 간단한 작업이 아니라

print("???") // 아주 오래 걸리는 작업
print("2")
print("3")

이렇게 아주 오래 걸리는 작업이 있을 경우 

뒤의 작업들이 앞의 작업이 끝날 때 까지 기다려야하는 상황이 생깁니다.

조금 기다리면 되는거 아닌가? 싶기도 하지만 사용자에게 보여지는 뷰에도 이런식으로 로딩이 길어지면 좋은 앱이라고 하기 어렵습니다.

 

그럼 어떻게 처리해야할까요?

그래서 우리는 비동기를 사용합니다.

 

async (비동기)

비동기는 동기와 다르게 앞에 오래 걸리는 작업이 있다면, 해당 부분을 비동기 처리해주어 다른 스레드에서 실행시키고,

뒤의 작업들을 처리해줍니다.

 

밑의 코드를 예시로 들면

print("1")
DispatchQueue.main.async {
	print("2")
}
print("3")

원래대로 라면 1 2 3이 순서대로 호출 되었겠지만 async 처리를 해주었기 때문에 

1 3 2 와 같이 호출될 수도 있습니다.

 

그럼 비동기 작업이 언제 끝나는지 확인할 수 있을까요?

Swift는 클로저를 사용해서 completionHandler를 통해 작업이 끝났는지 확인할 수 있습니다. 

 

하지만 밑의 작업처럼 다양한 오류에 대한 예외 처리가 많은 비동기 작업에는 completionHandler를 일일히 작성하기 어렵습니다.

// (2a) Using a `guard` statement for each callback:
func processImageData2a(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

processImageData2a { image, error in
    guard let image = image else {
        display("No image today", error)
        return
    }
    display(image)
}

 

async & await

async & await를 밑의 보기처럼 비동기 작업을 동기 작업처럼 작성할 수 있게 도와주고 훨씬 간결하게 사용할 수 있게 해줍니다.

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(_ i : Image) async throws -> Image

func processImageData() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

이런식으로 오류가 발생할 수 있는 함수는 async 뒤에 throws를 붙여 에러가 반환될 수 있다는 것을 알려주고,

그 함수 내부에 실제 에러가 발생되는 구문에는 await 앞에 try를 붙여 호출해줍니다.

 

async 함수를 호출하기 위해서는 await 키워드가 필요합니다.

await 지점은 potential suspension point 상태로 지정 되는데, 해당 함수가 await를 만나 suspend 상태가 되면 호출한 함수도 suspend상태가 됩니다.

 

이 말은 즉 async 함수를 호출하고 await를 만나는 순간 스레드 제어권은 system에게 넘어가게 됩니다.

그럼 system은 다른 작업들을 먼저 수행하게 되고, suspend 상태였던 async 함수가 실행되어도 판단되면 다시 스레드 제어권을 넘겨주어 resume 합니다. 

 

 

 

참고자료

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md#introduction, https://sujinnaljin.medium.com/swift-async-await-concurrency-bd7bcf34e26f