SwiftConcurrency 5편 - 구조화된 Task 보장하는것은? + Group Task

김재형·2024년 6월 19일
0
post-custom-banner

시작 하기에 앞서

해당 글은 스위프트 컨커런시 4편과 이어집니다. 물론 1편 부터 보시고 오시길 바랍니다.
시작하겠습니다.

구조화된 Task - Structured Concurrency 가 보장하는것이 무엇인가?

저번시간에 이어서 이제는 구조화가 된 Task 는 무엇을 보장하는지 알아보도록 하겠습니다.

import Foundation

func fetchData(url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

func performSequentialTasks() async {
    let url1 = URL(string: "https://KingJaeHyung.com/1")!
    let url2 = URL(string: "https://KingJaeHyung.com/2")!
    
    do {
        let data1 = try await fetchData(url: url1)
        print("Data 1: \(data1)")
        try await Task.sleep(for .second(2))
        let data2 = try await fetchData(url: url2)
        print("Data 2: \(data2)")
    } catch {
        print("Error: \(error)")
    }
}
Task {
    let task = Task {
        do {
            try await performSequentialTasks()
        } catch {
            print("Task was cancelled due to error: \(error)")
        }
    }
    // 1초 후에 작업을 취소할게요
    Task {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        task.cancel()
    }
}

저번 시간 코드를 가져와 보았습니다.
하지만 무언가 다른 부분이 있죠 위에 코드를 자세히 다시 봐보겠습니다.
상위의 테스크를 1초후에 취소 하도록 짜보았습니다.
async await는 하나의 Task 가 취소되면 해당 Task의 모든 하위 Task 자동으로 취소됩니다.

좀더 명확하게 생각해 보죠.
이렇게 작성해 보면 어떨까요?

import Foundation

func fetchData(url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

func performSequentialTasks() async throws {
    let url1 = URL(string: "https://KingJaeHyung.com/1")!
    let url2 = URL(string: "https://KingJaeHyung.com/2")!
    
    let data1 = try await fetchData(url: url1)
    print("Data 1: \(data1)")
    
    try await Task.sleep(for: .seconds(2))
    
    let data2 = try await fetchData(url: url2)
    print("Data 2: \(data2)")
}

Task {
    do {
        try await performSequentialTasks()
    } catch {
        print("Error: \(error)")
    }
}

1. 순차적 바인딩이다.

  • performSequentialTasks 함수는 fetchData 함수를 호출하고, await 키워드를 사용하여 비동기 작업이 완료될 때까지 기다립니다.
  • 첫 번째 데이터가 성공적으로 반환되면, 중간에 2초 지연 시킵니다.
  • 2초 후, 다시 fetchData 함수를 호출하고 await 키워드를 사용하여 비동기 작업이 완료될 때까지 기다립니다.
  • 두 번째 데이터가 성공적으로 반환될때, 출력합니다.

2. 만약 취소 처리가 일어났다면, 안정성은?

  • 구조화된 동시성은 작업이 취소 처리가 되었다면, 해당 Task 의 모든 하위 작업들도 모두 취소됨을 보장합니다.
  • 이는 ARC가 메모리 수명 즉 메모리 누수를 방지하는 것과 유사합니다.
  • 비동기 작업의 수명 관리도 자동으로 처리 됨으로, 실수로 작업이 종료 되지 않는 문제를 방지합니다.
import Foundation

func fetchData(url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// 무한 루프로 해보죠
func performInfiniteTasks() async throws {
    let url = URL(string: "https://KingJaeHyung.com/1")!
    
    while true {
        // 취소 여부 확인
        if Task.isCancelled {
            print("Task was cancelled")
            break
        }
        
        // 데이터 가져오기
        let data = try await fetchData(url: url)
        print("Data: \(data)")
        
        // 작업 사이에 1초 대기
        try await Task.sleep(for: .seconds(1))
    }
}

// 메인 실행
let task = Task {
    do {
        try await performInfiniteTasks()
    } catch {
        if Task.isCancelled {
            print("Task was cancelled")
        } else {
            print("Error: \(error)")
        }
    }
}

// 5초 후에 작업 취소
Task {
    try await Task.sleep(for: .seconds(5))
    task.cancel()
}

Group Task

이번엔 Group Task 에 대해서 알아보도록 하겠습니다.
여러개의 비동기 작업을 Group화 하여 관리할수 있는 기능
즉, 여러 비동기 작업을 병렬로 실행하고, 모든 작업이 완료될때까지 기다릴수도 있지만,
따로 결과를 처리할수도 있습니다.


func fetchData(url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// 배열로된 URL을 병렬로 처리할 함수
func fetchMultipleData(urls: [URL]) async throws -> [Data] {
    return try await withThrowingTaskGroup(of: Data.self) { group -> [Data] in
        var results = [Data]()
        
        // 각 URL 작업을 그룹에 추가
        for url in urls {
            group.addTask {
                return try await fetchData(url: url)
            }
        }
        
        // 각 비동기 작업의 결과를 수집
        for try await result in group {
            results.append(result)
        }
        
        return results
    }
}

Task {
	let urls = [
    	URL(string: "https://KingJaeHyung.com/1")!,
    	URL(string: "https://KingJaeHyung.com/2")!,
        URL(string: "https://KingJaeHyung.com/3")!,
        URL(string: "https://KingJaeHyung.com/4")!,
    ]
    do {
    	let results = try await fetchMultipleData(urls: urls)
        for (index, data) in results.enumerated() {
			print("결과 \(index + 1): \(data)")  
		}
    } catch {
    	print(error)
    }

withThrowingTaskGroup 를 통해 Task Group을 생성할수 있습니다.
그룹에 추가된 Task는 Group 블록 범위 만큼만 수명을 가질수 있습니다.
오류를 발생시킬수 있는 Task(하위) 를 위해 범위가 지정된 Group Object를 제공합니다.

마무리 하며....

현재 위 코드들은 특히 Group Task를 다루다가 @Sendalble에
대해서 다루고 나서 좀더 깊이 다루어야 겠다 라는 생각이 들어
이번편은 가볍게 여기까지 하도록 하겠습니다. 다음 편에선
@Sendable 과 Data Race Safety는 뭘까 를 다루어 보도록 하겠습니다.
감사합니다.

profile
IOS 개발자 새싹이
post-custom-banner

0개의 댓글