[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Concurrency & GCD

[Swift][문서의역] Task

inu 2022. 3. 8. 16:39
반응형

참고 및 출처

Task & TaskGroup

Task 인스턴스를 생성할 때 해당 Task가 수행할 작업을 포함하는 클로저를 함께 제공하게 됩니다. Task는 생성 직후 실행을 시작할 수 있습니다. 명시적으로 시작하거나 예약하지 않아도 됩니다. Task를 만들면 해당 인스턴스를 사용하여 작업과 상호작용합니다. 예를 들어 Task의 작업이 완료될 때까지 기다리거나 취소할 수 있습니다. Task의 작업이 완료될 때까지 기다리거나 취소할 때까지 기다리지 않고 Task에 대한 참조를 버리는 것은 프로그래밍적 에러가 아닙니다. Task는 참조 유지 여부에 관계없이 처리됩니다. 그러나 Task에 대한 참조를 버리면 해당 Task의 결과를 기다리거나 Task를 취소할 수 있는 기능을 포기하게 됩니다.

 

분리된 Task이나 Child Task가 될 수 있는 현재 Task의 작업을 지원하기 위해 Task는 yield()와 같은 클래스 메서드를 노출합니다. 이러한 메서드는 비동기식이므로 항상 기존 작업의 일부로 호출됩니다.

 

Task의 일부로 실행 중인 코드에서만 해당 Task와 상호 작용할 수 있습니다. 현재 Task와 상호 작용하려면 Task 내부에서 static 메서드 중 하나를 호출합니다.

withTaskGroup(of:returning:body:) 메서드를 호출해 Task Group을 생성할 수 있습니다.

 

Task Group을 생성한 Task 외부에서 Task Group을 사용하면 안됩니다. 대부분의 경우 Swift Type System은 Task Group에 Child Task를 추가하는 것이 일종의 변경 작업이고 Child Task와 같은 동시성 작업에서 이런 변경 작업을 수행할 수 없기 때문에 Task Group이 이러한 탈출을 방지하는 것을 방지합니다.


Task의 사용

일반적으로 Task는 다음과 같은 방법으로 생성합니다.

let basicTask = Task {
    return "This is the result of the task"
}

이렇게 생성된 Task는 이와 같은 방식으로 사용할 수 있습니다.

let basicTask = Task {
    return "This is the result of the task"
}
print(await basicTask.value)
// Prints: This is the result of the task

내부적으로 에러를 반환하도록 처리할수도 있습니다.

let basicTask = Task {
    // .. perform some work
    throw ExampleError.somethingIsWrong
}

do {
    print(try await basicTask.value)
} catch {
    print("Basic task failed with error: \(error)")
}

이처럼 Task를 활용해 Value를 생성할 수도 있고, Error를 발생시킬 수 있습니다. Task는 생성과 동시에 실행됩니다. 따로 실행처리를 해줄 필요 없습니다. 동기적으로 값과 에러를 반환하는 것과 별개로, Task는 async method도 실행할 수 있습니다. 우리는 concurrency를 지원하지 않는 함수 내부에서 async method를 실행시키려 할때 Task를 필요로 합니다.

 

아래 예시의 executeTask는 다른 Task를 Wrapping하는 method입니다.

func executeTask() async {
    let basicTask = Task {
        return "This is the result of the task"
    }
    print(await basicTask.value)
}

이를 바로 실행하려고하면 아래와 같은 에러가 발생합니다.

우리는 새로운 Task와 함께 executeTask를 호출하는 것으로 위에서 확인한 에러를 해결할 수 있습니다.

var body: some View {
    Text("Hello, world!")
        .padding()
        .onAppear {
            Task {
                await executeTask()
            }
        }
}

func executeTask() async {
    let basicTask = Task {
        return "This is the result of the task"
    }
    print(await basicTask.value)
}

해당 Task는 동시성(concurrency)을 지원하는 환경을 생성하고 우리는 그곳에서 async method인 executeTask를 호출합니다. 따로 생성된 Task에 대한 참조를 유지하지 않아도 실행됩니다. Combine은 값을 내보낼 수 있도록 Publisher subscription의 강한 참조를 유지할 것을 요구했습니다. Combine을 생각해보면, 모든 참조가 해제되면 Task 자체도 취소될 것으로 예상됩니다.

 

하지만 Task는 참조를 하고있는지 여부와는 관계없이 동작합니다. Task에 대한 참조 유지는 그에 대한 일시정지 혹은 취소를 원할 경우에만 필요로 합니다.

 

다음은 이미지를 로딩하는 예제입니다.

struct ContentView: View {
    @State var image: UIImage?

    var body: some View {
        VStack {
            if let image = image {
                Image(uiImage: image)
            } else {
                Text("Loading...")
            }
        }.onAppear {
            Task {
                do {
                    image = try await fetchImage()
                } catch {
                    print("Image loading failed: \(error)")
                }
            }
        }
    }

    func fetchImage() async throws -> UIImage? {
        let imageTask = Task { () -> UIImage? in
            let imageURL = URL(string: "https://source.unsplash.com/random")!
            print("Starting network request...")
            let (imageData, _) = try await URLSession.shared.data(from: imageURL)
            return UIImage(data: imageData)
        }
        return try await imageTask.value
    }
}

이 예제는 랜덤한 이미지를 fetch하고 이 작업이 성공했을 경우 바로 화면에 띄웁니다.

참조가 있기 때문에 우리는 imageTask를 바로 취소할 수 있습니다.

func fetchImage() async throws -> UIImage? {
    let imageTask = Task { () -> UIImage? in
        let imageURL = URL(string: "https://source.unsplash.com/random")!
        print("Starting network request...")
        let (imageData, _) = try await URLSession.shared.data(from: imageURL)
        return UIImage(data: imageData)
    }
    // Cancel the image request right away:
    imageTask.cancel()
    return try await imageTask.value
}

위의 cancel() 호출은 URLSession의 실행되기 전에 취소작업을 수행하기 때문에 작업에 대한 성공을 중지할 수 있습니다. 따라서 위의 코드는 다음을 출력합니다.

Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"

checkCancellation을 통해 작업의 취소여부를 확인할 수 있습니다.

let imageTask = Task { () -> UIImage? in
    let imageURL = URL(string: "https://source.unsplash.com/random")!

    /// Throw an error if the task was already cancelled.
    try Task.checkCancellation()

    print("Starting network request...")
    let (imageData, _) = try await URLSession.shared.data(from: imageURL)
    return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()

print문과 네트워크 요청이 모두 호출되지 않습니다.

Image loading failed: CancellationError()

isCancelled에 접근하여 취소여부를 확인할 수도 있습니다. 이를 통해 취소전 추가적인 작업을 처리할 수 있습니다.

let imageTask = Task { () -> UIImage? in
    let imageURL = URL(string: "https://source.unsplash.com/random")!

    guard Task.isCancelled == false else {
        // Perform clean up
        print("Image request was cancelled")
        return nil
    }

    print("Starting network request...")
    let (imageData, _) = try await URLSession.shared.data(from: imageURL)
    return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()

취소여부를 확인하는 것(checkCancellation)은 불필요한 작업을 방지하는데 도움이 됩니다.

let imageTask = Task { () -> UIImage? in
    let imageURL = URL(string: "https://source.unsplash.com/random")!

    // Check for cancellation before the network request.
    try Task.checkCancellation()
    print("Starting network request...")
    let (imageData, _) = try await URLSession.shared.data(from: imageURL)

    // Check for cancellation after the network request
    // to prevent starting our heavy image operations.
    try Task.checkCancellation()

    let image = UIImage(data: imageData)

    // Perform image operations since the task is not cancelled.
    return image
}

Dispatch Queue를 사용할 때 Qos를 설정한것과 유사한 방식으로 우선순위를 설정할 수 있습니다. 이를 통해 낮은 우선순위의 작업이 높은 우선순위의 작업을 막는 것을 방지할 수 있습니다.

반응형

'🍎 Apple > Concurrency & GCD' 카테고리의 다른 글

[Concurrency] async / await  (2) 2023.03.31
[Swift] GCD 정리하기  (2) 2022.05.30
[Swift] Operation Queue  (0) 2022.05.27
[Swift] Actor  (4) 2022.03.08
[Swift][문서의역] Concurrency (async & await)  (0) 2022.03.08