Concurrency를 공부하며

Dophi·2023년 1월 24일
0

코드 따라하기

목록 보기
3/5

소개글

Swiftful Thinking 이라는 유튜브 채널을 보며 Concurrency를 공부해봤습니다.
공부하면서 알아두면 좋겠다는 생각이 든 요소들을 공유하고자 합니다.
해당 영상은 아래 링크를 통해 볼 수 있으며, 비동기 처리, 그중에서도 async에 관심이 있다면 한번쯤 보시는 것을 추천드립니다!

영상 링크
코드

Do, Catch, Try

// throw를 통해 이 함수를 쓰는 부분에서 에러를 처리하게 함
func getTitle() throws -> String {
    if isActive {
        return "NEW TEXT!"
    } else {
        throw URLError(.badURL)
    }
}

do {
	// 에러가 날 경우 nil 값 반환, catch로 안빠짐
    let optionalTitle = try? manager.getTitle() 
    if let optionalTitle = optionalTitle {
        self.text = optionalTitle
    }
    // 에러가 날 경우 catch로 빠짐        
    let catchTitle = try manager.getTitle() 
    self.text = catchTitle
} catch {
    self.text = error.localizedDescription
}
  • 어떤 함수를 실행할 때 에러에 대해서 throw를 통해 이 함수를 쓰는 부분에서 처리하게 할 수 있습니다.
  • do, catch문으로 throw 함수를 처리할 수 있습니다.
  • 에러 발생시 try?는 그냥 nil값을 반환해주고, try는 더 이상 do 안쪽의 코드가 실행이 안되고 catch문으로 빠져나갑니다.
  • catch(let error) 로 쓸 수도 있고 변수명을 생략할 수도 있습니다.

비동기 처리 종류

// Escaping Closure
func downloadWithEscaping(completionHandler: @escaping ( _ image: UIImage?, _ error: Error?) -> ()) {
    URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
        let image = self?.handleResponse(data: data, response: response)
        completionHandler(image, nil)
    }
    .resume()
}

// Combine
func downloadWithCombine() -> AnyPublisher<UIImage?, Error> {
    URLSession.shared.dataTaskPublisher(for: url)
        .map(handleResponse)
        .mapError({ $0 })
        .eraseToAnyPublisher()
}

// Async
func downloadWithAsync() async throws -> UIImage? {
    do {
        // 다운 받을때까지 이 코드에서 흐름이 잠깐 멈춤
        let (data, response) = try await URLSession.shared.data(from: url)
        return handleResponse(data: data, response: response)
    } catch {
        throw error
    }
}
  • 비동기처리를 다루는 대표적인 방법들에는 Escaping Closure, Combine, Async가 있습니다.
  • async 함수에서 비동기 처리를 다루는 코드 부분에 await 키워드를 쓸 수 있는데, 비동기 처리가 끝날 때까지 코드의 흐름이 여기에서 잠깐 멈춥니다.
  • 이번 포스팅에서는 async 위주로 다루며 더 자세히는 이어서 설명하겠습니다.

Task


// 이미지 다운받는 비동기 처리
func fetchImage() async {
    do {
    	...
        let (data, _) = try await URLSession.shared.data(from: url)
		...
    } catch {
        print(error.localizedDescription)
    }
}

// Task 안에 있는 코드들은 순서대로 진행됨
Task {
    await viewModel.fetchImage()
    await viewModel.fetchImage()
}
// 다른 Task와는 별개로 실행됨
Task {
    await viewModel.fetchImage()
    await viewModel.fetchImage()
}
// Task에 우선순위를 지정 가능함
Task(priority: .high) {
	...
}
// Task를 취소할 수 있음
let myTask = Task(priority: .userInitiated) {
	...
}
myTask.cancel()
  • 첫번째 Task와 두번째 Task는 별개로 실행됩니다. 즉 첫번째 Task 안쪽 코드가 아직 끝나지 않았더라도 두번째 Task는 실행됩니다.
  • Task 안쪽 코드들은 순서대로 실행되며, await을 만나면 그 코드에서 잠깐 멈췄다가 비동기 처리가 끝나면 계속 진행합니다.
  • 어떤 Task가 먼저 처리되게 할지 우선순위를 정해줄 수 있습니다. 다만 어디까지나 동시에 실행되고 있을 때의 기준이기 때문에, 우선순위가 아무리 높다해도 코드가 끝쪽에 있으면 늦게 실행됩니다.
  • Task가 실행중이더라도 중간에 취소 가능합니다. 화면이 사라지거나 했을 때 cancel 코드를 넣어주면 유용할 것입니다.
  • SwiftUI에서는 .task로 쓸 수도 있는데, 이는 화면이 사라졌을 때 자동으로 취소시켜주는 기능도 포함되어있으므로 따로 코드를 쓸 필요 없습니다.

Async Let

/*
// 차례대로 기다리면서 실행함 -> 모든 비동기 처리를 동시에 하고 싶으면?
Task {
    do {
        let image1 = try await fetchImage()
        self.images.append(image1)
        
        let image2 = try await fetchImage()
        self.images.append(image2)
        
        let image3 = try await fetchImage()
        self.images.append(image3)
        
        let image4 = try await fetchImage()
        self.images.append(image4)
    } catch { }
}
 */
Task {
    do {
        // async let 으로 함수를 정의하고, await로 묶기
        async let fetchImage1 = fetchImage()
        async let fetchImage2 = fetchImage()
        async let fetchImage3 = fetchImage()
        async let fetchImage4 = fetchImage()
        
        // 네가지 비동기 처리가 모두 끝날때까지 기다렸다가 다음 코드를 실행하기
        let (image1, image2, image3, image4) = await (try fetchImage1, try fetchImage2, try fetchImage3, try fetchImage4)
        self.images.append(contentsOf: [image1, image2, image3, image4])
    } catch { }
}
  • 지금까지는 하나의 비동기 처리를 완료하고 다음 비동기 처리를 진행하는 방식이었습니다.
  • 만약 여러가지 비동기 처리를 동시에 실행하고, 모두 다 처리가 된 다음 진행하고 싶다면 async let을 사용할 수 있습니다.

TaskGroup

func fetchImageWithTaskGroup() async throws -> [UIImage] {
    return try await withThrowingTaskGroup(of: UIImage?.self) { group in
        var images: [UIImage] = []
        
        for urlString in urlStrings {
        	// 처리하고 싶은 Task들을 추가함
            group.addTask {
                // 실패하면 나머지 비동기 처리도 끊김
                // try await self.fetchImage(urlString: urlString)
                
                // 실패해도 그 비동기 처리만 nil 반환함
                try? await self.fetchImage(urlString: urlString)
            }
        }
        
        // 일반적인 for문이 아님
        // 모든 task가 끝날때까지 기다리는 for문임
        for try await image in group {
            if let image = image {
                images.append(image)
            }
        }
        
        return images
    }
}
  • 앞선 예제에서 async let으로 4개의 비동기 처리를 선언하고 처리할 수 있었지만, 만약 비동기 처리가 1000개라면 이런 방식은 불가능합니다.
  • 이는 taskGroup을 통해 해결할 수 있습니다.
  • withTaskGroup 이라는 문법을 사용 가능하며, 에러를 처리하고 싶다면 withThrowingTaskGroup을 쓰면 됩니다.
  • addTask를 통해 처리하고 싶은 Task를 추가할 수 있습니다.
  • 이후 for문이 있는데, 이건 일반적인 for문이 아니라 추가했던 모든 Task가 끝나길 기다리는 for문입니다.

CheckedContinuation

// completion handler를 썼을 경우
func getHeartImageFromDatabase(completionHandler: @escaping (_ image: UIImage) -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now()+5) {
        completionHandler(UIImage(systemName: "heart.fill")!)
    }
}

// continuation을 썼을 경우 (async await)
func getHeartImageFromDatabase() async -> UIImage {
    await withCheckedContinuation { continuation in
        DispatchQueue.main.asyncAfter(deadline: .now()+5) {
            continuation.resume(returning: UIImage(systemName: "heart.fill")!)
        }
    }
}

...
.task {
    self.image = await getHeartImageFromDatabase()
}
  • async를 쓰고 싶은데, 특정 비동기 처리가 async를 지원하지 않고 있을 수도 있습니다.
  • 이럴 때 withCheckedContinuation을 이용해 async 함수가 되도록 만들어줄 수 있습니다.
  • 에러를 처리하고 싶다면 withCheckedThrowingContinuation을 쓸 수 있습니다.
  • escaping closure에서 completion handler와 비슷하게, checkedContinuation에서도 resume으로 빠져나가고 싶은 지점을 결정해줄 수 있습니다.

Actor

// class와 거의 같음
// + thread-safe 특성을 가지고 있음 (await)
actor MyActor {
    var title: String
    
    init(title: String) {
        self.title = title
    }
    
    func updateTitle(newTitle: String) {
        title = newTitle
    }
    
    // await가 필요없음
    nonisolated func updateTitleWithoutWait(newTitle: String) {
        title = newTitle
    }
}

private func actorTest1() {
    Task {
        let object = MyActor(title: "first title")
        // actor 프로퍼티에 접근할때마다 await 해줘야함
        await print("Object: ", object.title)
        await object.updateTitle(newTitle: "second title")
        await print("Object: ", object.title)
        object.updateTitleWithoutWait(newTitle: "third title")
    }
}
  • swift에는 actor라는 키워드가 있습니다.
  • 클래스와 매우 비슷한데, 유일하게 다른점은 스레드로부터 안전하다는 점입니다.
  • actor에 있는 변수나 메소드에 접근할 때는 무조건 await을 써줘야 합니다.
  • 만약 다른 스레드가 해당 변수나 메소드를 사용하고 있으면 await에 걸리게 되고 끝난 후에야 이어서 사용 가능하기 때문에 동기화에 관해서 안전합니다.
  • nonisolated 키워드를 쓰면 await을 할 필요가 없습니다.

많이 쓰일 것 같은 문법에 대해서만 설명을 했지만, 실제 영상에는 좀 더 다양하고 어려운 문법에 대해 설명해주십니다.
관심이 있다면 한번 봐보시면 좋을 듯 합니다!

profile
개발을 하며 경험한 것들을 이것저것 작성해보고 있습니다!

0개의 댓글