저번 포스팅에서 살펴보았던 Structured Concurrency는 Task는 부모 - 자식 관계를 통해 계층구조로 이루어져 있다.

 

하지만, 특정 Task에는 structured pattern이 없는 경우가 있다.

대표적인 예로, 동기 context에서 실행되는 비동기 작업들은 Parent Task가 존재하지 않는다. 

이러한 경우에 Swift에서는 Unstructured Concurrency를 제공한다.

 

Unstructured Concurrency

Unstructured Task들은 life time이 그들의 scop보다 오래 존재할 수 있다. 

이미지를 다운로드 Task가 있고, 이 Task를 중간에 cancel 시켜 다운로드를 취소시킬 수 있는 기능을 제공할 수 있다.

 

또한, Parent Task가 없기 때문에 상위 Task를 cancel시켜도 propageted되지 않는다.

func someAsyncFunc() async -> String {
    sleep(2)
    return "123"
}

func someThrowError() async throws  {
    sleep(3)
    
    var unstructuedTask = Task { //
        print("Unstructued Task")
    }

    async let res2 = someAsyncFunc()
    
    print("structured Task Cancellation : \(Task.isCancelled)")
    print("unstructued Task Cancellation : \(unstructuedTask.isCancelled)")
    
    try Task.checkCancellation()
}

var someTask = Task(priority: .background) {
    try await someThrowError()
}

print("task cancellation")
someTask.cancel()


/* Prints:
 task cancellation
 structured Task Cancellation : true
 unstructued Task Cancellation : false
 Unstructued Task
*/

 

동기 Context에서 async Task

앞선 포스팅에선 이를 간단하게 다루었으나, 이를 좀더 자세하게 다뤄보자. 

async 작업들은 비동기 context에서 수행가능하기 때문에,

Task{ } 를 block을 통해 동기 context에서의 수행을 가능하게 했다.

 

Task { }는 explicit하게 task를 생성한다.

또한, Task는 concurrent하게 실행하기 때문에 Task { } block 아래의 코드와 concurrent하게 실행된다. 

func someTask() async -> String {
    sleep(3)
    print("in task function")
    return "123"
}

Task {
    let res = await someTask()
    print("after tast")
}
print("outside of Task block")

/* Prints:
 outside of Task block
 in task function
 after tast
*/

 

Task { } 로 Task를 생성하게 되면 Task<T, Error> 타입을 리턴하며,

이를 통해 explicit하게 cancel과 await등 여러 가지 옵션사용이 가능하다.

func someTask() async throws -> String {
    sleep(3)
    try Task.checkCancellation()
    print("in task function")
    return "123"
}

var taskHandler: Task<Void, Never>?

taskHandler = Task {
    do {
        let res = try await someTask()
        print("after tast: \(res)")
    }
    catch {
        print("task cancellation")
    }
    taskHandler = nil
}

if taskHandler != nil {
    print("cancel")
    taskHandler?.cancel()
}
print("outside of Task block")

/* Prints:
 cancel
 outside of Task block
 task cancellation
*/

 

Detached Tasks

Unstructured Concurrecny를 제공하는 Detached Tasks가장 높은 flexibility를 제공한다. 

Unstructured Task이기에,

언제 어디서든 비동기 코드를 실행할 수 있고,

lifetime이 그들의 scope보다 오래 존재할 수 있으며, 

manually하게 cancel과 await할 수 있다.

 

또한, Detached Tasks는 origination scope으로부터 어떠한 것도 상속받지 않는다.

심지어 actor와 prioirty조차도 상속받지 않는다. 

따라서 orination scope과 완전히 독립적인 작업을 할 때 유용하다.

var taskHandler: Task<Void, Never>?

taskHandler = Task.detached(priority: .background) {
    do {
        let res = try await someTask()
        print("after tast: \(res)")
    }
    catch {
        print("task cancellation")
    }
    taskHandler = nil
}

if taskHandler != nil {
    print("cancel")
    taskHandler?.cancel()
}
print("outside of Task block")

/* Prints:
 cancel
 outside of Task block
 task cancellation
*/

사용법은 Task { }와 별 다를 것 없지만, 

Detached Task는 어떠한 것도 상속받지 않는다는 차이점이 있다.

 

Swift에선 MainActor라는 것이 있는데, 함수에 @MainActor annotation을 붙이게 되면

해당 함수는 Main Thread에서 동작하게 된다. 

(Main Actor에 대한 자세한 내용은 해당 포스팅을 참고 바란다.)

@MainActor func someFunction() {
    Task {
        // Main Thread에서 동작
    }
    
    Task.detached(priority: .background) {
        // 어떠한 것도 상속 X -> Main Thread에서 동작하지 않는다.
    }
}

Task { }와 다르게 Detached Task는 actor조차도 상속받지 않기 때문에, 

Main Thread에서 진행하지 않아도 될 작업들을 수행할때 유용하다.

 

Reference

WWDC 2021 

 

Apple Documentation 

 

Introduction to Unstructured Concurrency in Swift - Andy Ibanez

Unstructured Concurrency With Detached Tasks in Swift - Andy Ibanez

복사했습니다!