Swift 작업과 작업 그룹 (Tasks and Task Groups)

Tabber·2024년 7월 11일
0

Swift Concurrency

목록 보기
2/3
post-thumbnail
post-custom-banner

동시성 (Concurrency) | Swift

⚠️ 해당 내용은 Swift 공식문서인 동시성을 한글로 번역되어있는 내용을 바탕으로 작성한 글입니다. (말이 바탕이지 사실상 복사 붙혀넣기 입니다.)

이전에 작성한 글도 확인해보세요!

Swift Concurrency 1 - 동시성, 비동기 시퀀스

작업과 작업 그룹 (Tasks and Task Groups)

작업(Task)은 프로그램의 일부로 비동기적으로 실행할 수 있는 작업 단위 입니다.

모든 비동기 코드는 어떠한 작업의 일부로 실행이 됩니다. 작업은 한번에 하나의 작업만 수행하지만, 여러 작업을 생성하면 Swift는 동시에 수행하기 위해서 작업을 스케줄링 할 수 있습니다.

위에서 설명한 async-let 구문은 사실 하위 작업을 생성하게 됩니다. (이 구문은 프로그램에서 어떤 작업을 수행해야 될지 이미 알고 있을 때 더 잘 동작한다고 하네요!)

작업 그룹(Task group)(TaskGroup 의 인스턴스) 을 생성하고 우선순위와 취소를 더 잘 제어할 수 있고, 동적으로 작업의 수를 생성할 수 있도록 해당 그룹에 하위 작업을 명시적으로 추가할 수도 있습니다.

작업은 계층 구조로 정렬 됩니다. 주어진 작업 그룹의 각 작업에는 동일한 상위 작업이 있으며, 각 작업에는 하위 작업이 있을 수도 있어요.

작업과 작업 그룹 간의 명시적인 관계 때문에 이 접근 방식을 구조적 동시성 (structured concurrency) 이라고 부른다고 합니다. 명시적 부모 - 자식 관계는 작업간에 여러 이점이 존재합니다.

  • 부모 작업에서, 하위 작업이 완료될 때까지 기다릴 수 있습니다.
    예를 들어, 다음 코드에서는 부모 작업이 모든 하위 작업이 완료될 때까지 기다립니다:
func parentTask() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            // 하위 작업 1
        }
        group.addTask {
            // 하위 작업 2
        }
    }
    // 모든 하위 작업이 완료된 후에 실행되는 코드
}
  • 하위 작업에서 더 높은 우선순위로 설정되면, 상위 작업의 우선순위는 자동으로 높아집니다.
  • 상위 작업이 취소가 되면, 각 하위 작업들도 자동으로 취소가 됩니다.
    예를 들어, 사용자가 화면을 벗어날 때 해당 화면과 관련된 모든 비동기 작업이 취소가 될 수 있습니다.
func parentTask() async {
    do {
        try await withThrowingTaskGroup(of: Void.self) { group in
            group.addTask {
                // 하위 작업 1
            }
            group.addTask {
                // 하위 작업 2
            }
            throw CancellationError()
        }
    } catch {
        // 상위 작업이 취소되었을 때 처리
    }
}
  • 작업-로컬 값 (Task-local value) 는 하위 작업에 효율적이고 자동으로 전파가 됩니다.
    예를 들어 특정 사용자 세션 정보를 작업-로컬 값으로 설정하면, 해당 세션 정보는 모든 하위 작업에서도 접근이 가능합니다.
enum Session {
    @TaskLocal static var current: String?
}

func parentTask() async {
    await Session.$current.withValue("UserSession") {
        await withTaskGroup(of: Void.self) { group in
            group.addTask {
                print(Session.current) // "UserSession"
            }
        }
    }
}

예시 코드를 한번 봅시다.

await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "여름 휴가")
    for name in photoNames {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }

    for await photo in group {
        show(photo)
    }
}

위 코드는 여러 사진을 다운로드 하는 코드입니다. 코드는 새로운 작업 그룹을 생성하고, 갤러리에 사진을 다운로드 하는 하위 작업을 생성했습니다. Swift는 조건이 되는만큼 비동기적으로 여러 작업을 수행합니다. 하위 작업에서 사진 다운로드가 끝나자마자 해당 사진은 보여지게 되죠. 단, 하위 작업 완료에 대한 순서를 보장하지 않기에 갤러리에 사진은 무작위로 보여질 수 있습니다.

조금 다른 결로 결과를 반환해야 하는 경우에는 withTaskGroup 에 전달하는 클로저에 결과를 더하는 코드를 추가하면 됩니다.

let photos = await withTaskGroup(of: Data.self) { group in
    let photoNames = await listPhotos(inGallery: "여름 휴가")
    for name in photoNames {
        group.addTask {
            return await downloadPhoto(named: name)
        }
    }

    var results: [Data] = []
    for await photo in group {
        results.append(photo)
    }

    return results
}

방금 위의 코드와 같이, 이 코드도 각 사진을 다운로드하는 하위 작업을 생성하게 됩니다. 그러나 for-await-in 루프는 다음 하위 작업이 완료될 때까지 기다리고 난 다음, 해당 작업의 결과를 배열에 추가하고 기다립니다. 그리고 코드는 마지막으로 다운로드한 사진의 배열을 전체 결과로 반환하게 됩니다.

이렇게 Task를 그룹단위로 묶어서 사용할 수도 있습니다.

작업 취소 (Task Cancellation)

Swift 동시성은 협동 취소 모델 (cooperative cancellation model) 을 사용합니다…

💡 하..또 이상한 말 쓰길래 이해 1도 안되서 찾아봤습니다.(지들만 이해 가능한 말 또 쓰지)

협동 취소 모델은 Swift 동시성에서 작업을 취소하는 방식입니다. 이 모델은 시스템이나 프레임워크가 작업을 강제로 중단시키는 대신에, 작업 자체가 스스로 취소 신호를 감지하고 스스로 중단하도록 설계가 된 모델입니다. (똑똑하네요)

협동 취소 모델의 주요 개념은 다음과 같습니다.
1. 취소 신호

  • 작업이 취소되기를 원할 때, 시스템은 해당 작업에 취소 신호를 보내게 됩니다.
  • 이 신호는 작업이 취소되어야 한다는 사실을 작업에게 알려주는 역할을 합니다.
  1. 자발적 취소
  • 작업이 취소 신호를 받으면, 작업은 주기적으로 이 신호를 확인하고, 필요한 경우 안전하게 자신을 중단합니다.
  • 작업은 실행 중에 적절한 시점에서 취소 여부를 확인하고 스스로 중단할 준비를 합니다.

협동 취소 모델 예시 코드들

  1. 취소 신호
    • Swift에서 Task 객체를 통해 작업을 생성하고, 해당 작업을 취소 할 수 있습니다.
    import Foundation
    
    // 네트워크 요청을 시뮬레이션하는 함수
    func fetchData(from url: URL) async throws -> Data {
        // 2초 동안 대기하여 네트워크 요청을 시뮬레이션
        try await Task.sleep(nanoseconds: 2_000_000_000)
        return Data()
    }
    
    // 데이터를 처리하는 함수
    func processData() async {
        let url = URL(string: "https://example.com/data")!
        
        // 취소 가능 작업 생성
        let task = Task {
            do {
                let data = try await fetchData(from: url)
                print("Data received: \(data)")
            } catch {
                print("Task was cancelled")
            }
        }
        
        // 1초 후 작업 취소
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        task.cancel() // 작업 취소
    }
    
    // 메인 함수 실행
    Task {
        await processData()
    }
  1. 자발적 취소
    • 자발적 취소는 작업 자체가 취소 신호를 받고 주기적으로 이를 확인해서 필요한 경우 중단시키는 과정입니다.
    import Foundation
    
    // 긴 계산 작업을 시뮬레이션하는 함수
    func performLongRunningTask() async {
        for i in 1...10 {
            // 취소 여부를 주기적으로 확인
            if Task.isCancelled {
                print("Task was cancelled at iteration \(i)")
                return
            }
            
            // 각 반복마다 1초 대기하여 작업 시뮬레이션
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            print("Iteration \(i)")
        }
        print("Task completed")
    }
    
    // 작업을 관리하는 함수
    func manageTask() async {
        let task = Task {
            await performLongRunningTask()
        }
        
        // 5초 후 작업 취소
        try? await Task.sleep(nanoseconds: 5_000_000_000)
        task.cancel()
    }
    
    // 메인 함수 실행
    Task {
        await manageTask()
    }
    

작업의 종류에 따라서 취소에 대한 응답은 다음 중 하나에 해당한다고 합니다.

  • CancellationError 와 같은 에러 발생
  • nil 또는 빈 컬렉션 반환
  • 부분적으로 완료된 작업의 반환

만약 사진이 크거나, 네트워크가 느려서 사진을 다운로드하는데 오랜 시간이 걸리는 경우, 모든 작업이 완료되기를 기다리지 않고 작업을 멈추려고 하면, 작업이 취소되었는지를 확인하고 취소된 경우에 실행을 중지할 수 있습니다.

작업을 취소되었는지 확인하는 방법은 두가지가 있습니다.

  • Task.checkCancellation() 타입 메서드를 호출
import Foundation

// 비동기 사진 다운로드 함수
func downloadPhoto(url: URL) async throws -> Data {
    // 네트워크 요청을 시뮬레이션 (5초 대기)
    print("Download Start", Date())
    
    try await Task.sleep(nanoseconds: 5_000_000_000)
    
    // 작업 취소 여부 확인
    try Task.checkCancellation()
    
    // 사진 데이터를 반환
    return Data()
}

// 여러 사진을 다운로드하는 함수
func downloadPhotos() async {
    let urls = [
        URL(string: "https://example.com/photo1")!,
        URL(string: "https://example.com/photo2")!
    ]
    
    do {
        for url in urls {
            let data = try await downloadPhoto(url: url)
            print("Downloaded photo from \(url)")
        }
    } catch is CancellationError {
        print("Task was cancelled", Date())
    } catch {
        print("An error occurred: \(error)")
    }
}

// 메인 함수 실행
Task {
    let downloadTask = Task {
        await downloadPhotos()
    }
    
    // 1초 후 작업 취소
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    downloadTask.cancel()
}

// 실행 결과
// Download Start 2024-06-25 06:23:19 +0000
// Task was cancelled 2024-06-25 06:23:21 +0000

Task.checkCancellation()을 호출하면 작업이 취소된 경우 CancellationError를 던집니다. 이를 통해 작업이 취소되었을 때 자동으로 예외를 발생시키고, 에러를 잡아서 처리할 수 있습니다.

  • Task.isCancelled 타입 프로퍼티 확인
import Foundation

// 비동기 사진 다운로드 함수
func downloadPhoto(url: URL) async throws -> Data? {
    var data = Data()
    for _ in 1...5 {
        // 작업 취소 여부 확인
        if Task.isCancelled {
            print("Task was cancelled, cleaning up")
            // 여기서 네트워크 연결을 닫거나 임시 파일 삭제 등의 정리 작업 수행 가능
            return nil
        }
        
        // 네트워크 요청을 시뮬레이션 (1초 대기)
        try await Task.sleep(nanoseconds: 1_000_000_000)
        data.append(Data())
    }
    
    // 사진 데이터를 반환
    return data
}

// 여러 사진을 다운로드하는 함수
func downloadPhotos() async {
    let urls = [
        URL(string: "https://example.com/photo1")!,
        URL(string: "https://example.com/photo2")!
    ]
    
    for url in urls {
        do {
            let data = try await downloadPhoto(url: url)
            if let data {
                print("Downloaded photo from \(url)")
            }
            
        } catch {
            print("An error occurred: \(error)")
        }
    }
}

// 메인 함수 실행
Task {
    let downloadTask = Task {
        await downloadPhotos()
    }
    
    // 2초 후 작업 취소
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    downloadTask.cancel()
}

// 실행 결과
// An error occurred: CancellationError()
// Task was cancelled, cleaning up

Task.isCancelled를 사용하면 작업이 취소되었는지 확인할 수 있습니다. 이 방법을 사용하면 작업이 취소된 경우 네트워크 연결을 닫거나 임시 파일을 삭제하는 등의 정리 작업을 수행할 수 있습니다.

즉시 취소에 대한 알림이 필요한 경우에는 Task.withTaskCancellationHandler() 메서드를 사용합니다.

import Foundation

// 비동기 사진 다운로드 함수
func downloadPhoto(url: URL) async throws -> Data {
    var data = Data()
    
    // Task.withTaskCancellationHandler 사용
    let result = try await withTaskCancellationHandler {
        for _ in 1...5 {
            // 작업 취소 여부 확인
            if Task.isCancelled {
                print("Task was cancelled, cleaning up")
                // 네트워크 연결을 닫거나 임시 파일 삭제 등의 정리 작업 수행 가능
                throw CancellationError()
            }
            
            // 네트워크 요청을 시뮬레이션 (1초 대기)
            try await Task.sleep(nanoseconds: 1_000_000_000)
            data.append(Data())
        }
        
        // 사진 데이터를 반환
        return data
    } onCancel: {
        // 작업이 취소되었을 때 실행될 코드
        print("Canceled!")
    }
    
    return result
}

// 여러 사진을 다운로드하는 함수
func downloadPhotos() async {
    let urls = [
        URL(string: "https://example.com/photo1")!,
        URL(string: "https://example.com/photo2")!
    ]
    
    for url in urls {
        do {
            let data = try await downloadPhoto(url: url)
            print("Downloaded photo from \(url)")
        } catch is CancellationError {
            print("Task was cancelled")
            return
        } catch {
            print("An error occurred: \(error)")
        }
    }
}

// 메인 함수 실행
Task {
    let downloadTask = Task {
        await downloadPhotos()
    }
    
    // 2초 후 작업 취소
    try? await Task.sleep(nanoseconds: 2_000_000_000)
    downloadTask.cancel()
}

// 실행 결과
// Canceled!
// Task was cancelled

취소 처리를 사용할 때, 작업 취소는 여전히 협조적입니다.

작업은 완료될 떄까지 수행하거나 취소를 확인하고 조기에 중지합니다.

취소 처리가 시작될 때 작업은 여전히 수행 중이므로,
경쟁 조건(race condition) 이 생성될 수 있는 작업과 취소 처리간의 상태 공유를 꼭 피해야 합니다.

💡 하..또 이상한 말 쓰길래 이해 1도 안되서 찾아봤습니다. 뭘 협조적이야 협조적이긴.. ㅇ룸너ㅏㅇㄴ

"작업 취소는 여전히 협조적입니다"라는 말은 Swift의 동시성 모델에서 작업이 취소되었을 때 시스템이 강제로 작업을 중단시키지 않고, 작업 자체가 주기적으로 취소 여부를 확인하고 필요할 때 스스로 중단하도록 한다는 의미입니다. 이 모델은 작업이 취소 신호를 받고, 스스로 적절한 시점에 취소를 처리하도록 하는 방식을 말합니다.

협소적 취소 모델의 의미

  • 자발적 확인: 작업은 주기적으로 취소 신호를 확인합니다. Task.isCancelled 또는 Task.checkCancellation()을 사용하여 작업이 취소되었는지 확인하고, 필요한 경우 스스로 중단합니다.
  • 안전한 중단: 작업은 취소 신호를 받은 즉시 중단되지 않습니다. 대신, 작업은 안전하게 중단될 수 있는 적절한 지점을 찾아 스스로 중단합니다.
  • 경쟁 조건 회피: 작업과 취소 핸들러 간의 상태 공유로 인한 경쟁 조건을 피해야 합니다. 작업이 취소될 때 여전히 수행 중일 수 있으므로, 상태를 안전하게 관리해야 합니다.

위에서는 협동 취소 모델 이라는 단어를 한참 쓰다가 무슨 듣도 보도 못한 협조적 취소 모델이 나오길래 빡쳐서 이 둘의 차이를 찾아봤습니다.

"협동 취소 모델"과 "협조적 취소 모델"은 사실 같은 개념을 설명하는 두 가지 다른 용어입니다.. ^^7

둘 다 비동기 프로그래밍에서 사용되는 개념으로, 작업이 취소될 때 작업이 스스로 취소 신호를 감지하고 적절히 처리하도록 하는 모델을 말합니다.

이 모델에서는 시스템이 강제로 작업을 중단시키는 대신, 작업 자체가 주기적으로 취소 여부를 확인하고 필요한 경우 스스로 중단합니다.

같은 말이랍니다~

보통은 협조적 취소 라는 용어를 사용한다고 합니다.


다음 글에서는 액터에 대해 파보도록 하죠!

profile
iOS 정복중인 Tabber 입니다.
post-custom-banner

0개의 댓글