[swift] 동시성 코드 (1/2)

이은수, Lee EunSoo·2024년 9월 26일
0

Swift Basic

목록 보기
15/24
post-thumbnail

개요

swift의 동시성이란 한 프로세스에 여러개의 작업을 수행하는 형태를 생각하면 된다 스레드와 비슷한 내용이다.

swift코드의 경우 비동기 코드와 병렬 코드가 있는데 비동기 코드의 경우 동시작업을 허용하지 않는 코드이고 병렬코드는 앞서 말한것처럼 동시에 여러개의 작업을 수행하는 코드를 말한다.

예를 들어라면을 끓여 먹는다고 하면
냄비에 물넣고 → 스프넣고 → 면넣고 → 뚜껑닫고 끓이기
위와 같은 일련의 과정에 의해 끓이게 될것이다.

만약 일반 함수의 경우 스프넣는 과정에서 흘리거나 스프를 못찾으면 라면조리과정은 여기서 멈추게 된다. (동기 코드)

(Asynchronous code) 비동기 코드의 경우 앞서 말한 라면조리 과정을 수행하는데 중간에 스프를 찾는동안 작업을 멈추지 말고 물도 넣고, 면도 넣고 다른 작업을 수행 하게 해준다.

(Parallel code) 병렬코드의 경우 비동기와 비슷하지만 조리과정에서 냄비에 면, 물, 스프를 동시에 넣는 조리이다.

하지만 위처럼 동시에 작업을 처리하면 면넣는데 물이나 스프를 넣어서 물이넘치거나 스프가 주면으로 튀는 등 주방이 개판나거나 라면이 망할 수 있는데(등짝 조심)

동시성 코드에서도 비슷하게 데드락같은 오류가 발생할 수 있으므로 동시에 작업하는 코드에 우선순위나 동기화 지점을 지정해야 한다.(컴파일러 강제)

여담

swift5.5이전까지는 RX-Swift나 Combine, api등을 사용해서 스레드를 관리했어야 했다.

WWDC2021에서 async-await방식의 동시 코드 처리방법이 공개 되었고 이번 시간에는 이 async-await를 이용한 동시성 코드처리에 대해 알아보려 한다.

비동기 함수

이전의 라면조리에서 설명한것처럼 일반 함수의 경우 하나의 단계가 끝날때 까지 다른작업을 못한다.

작업에 시간이 오래 걸리는 경우 비동기를 함수에 명시하고 함수가 실행되는 동시에 다른 작업을 수행할 수 있게 해준다.

사용법

선언

func someAsyncFunc() async -> String{
	return "hello world"
}

에러처리에서 함수에 던기지함수를 지정하는것처럼 함수의 return값 표시하는곳인 -> 앞에 async키워드를 붙이는것으로 비동기 함수임을 나타낸다.

호출

await someAsyncFunc()

다음과 같이 비동기 함수를 호출하는 경우 무조건 await키워드를 이용해야 한다.

예시

예시코드를 보고 일반함수와 비동기 함수의 차이점을 보자

//일반 함수
func fetchData() -> String {
    // 서버로부터 데이터를 동기적으로 받아옴
    let data = try! String(contentsOf: URL(string: "https://example.com")!)
    return data
}

Task{
	let data = fetchData() // 데이터를 다 가져올 때까지 대기
	print("네트워크 작업 완료 후 실행됨 1")
}
print("네트워크 작업 완료 후 실행됨 2")

//<결과>
//네트워크 작업 완료 후 실행됨 1
//네트워크 작업 완료 후 실행됨 2

//비동기 함수
func fetchDataAsync() async -> String {
    let url = URL(string: "https://example.com")!
    let (data, _) = try! await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? "데이터 없음"
}

Task {
    let data = await fetchDataAsync()  // 데이터를 기다림
    print("비동기 네트워크 작업 완료 후 실행됨")
}
print("네트워크 작업 중 다른 코드가 실행됨")


//<결과>
//네트워크 작업 중 다른 코드가 실행됨
//비동기 네트워크 작업 완료 후 실행됨

위 코드는 네트워크에서 데이터를 받아와 인코딩하는 함수이다 내용에 세세한 코드보다는 흐름에 집중해서 보자

네트워크에서 데이터를 받아오는 작업은 비동기코드를 사용하는 대표적인 사례이다.

네트워크 데이터 받아오기는 내부 데이터 처리속도보다 상대적으로 느린게 대부분이기 때문에 비동기로 처리를 많이 한다.


//비동기 던지기 함수
func listPhotos(inGallery name: String) async throws-> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

비동기 던기지 함수의 경우 throws 전에 async 를 작성한다.

비동기 함수의 호출

let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]

비동기 함수를 호출하는 경우 반드시 함수호출 앞에 await키워드를 붙여줘야 한다.

사용하는 이유는 컴파일러가 await키워드 명시를 강제하기도 하지만 중단지점을 표시하는 것 이라고 생각하면 된다.(코드 가독성 증가)

비동기 시퀀스

비동기 코드를 사용할때 비동기적인 데이터를 반복처리해야하는 상황이 오면 for-await-in 형식의 비동기 시퀀스를 사용하는게 도움이 될 수 있다.

사용은 기존의 for-in 반복문의 형태와 비슷하게 사용한다.

for await element in asyncSequence {
    // 각 element를 처리하는 코드
}

비동기 시퀀스를 사용하려면 asyncSequence부분에 AsyncSequence프로토콜을 따르는 인스턴스가 와야 한다.

AsyncSequence프로토콜은 비동기 작업을 지원하는 필수 프로토콜로, AsyncIterator를 통해 데이터를 비동기적으로 순차적으로 제공하도록 하는 프로토콜이다.

사용예시

struct MyAsyncSequence: AsyncSequence {
    typealias Element = Int
    
    struct AsyncIterator: AsyncIteratorProtocol {
        private var current = 0
        
        mutating func next() async -> Int? {
            current += 1
            if current <= 5 {
                await Task.sleep(1 * 1_000_000_000) // 1초 대기
                return current
            } else {
                return nil
            }
        }
    }
    
    func makeAsyncIterator() -> AsyncIterator {
        return AsyncIterator()
    }
}

Task {
    for await number in MyAsyncSequence() {
        print(number)
    }
}

//  결과
//  1
//  2
//  3
//  4
//  5

애플 공식문서에 따르면 "네트워크 데이터들을 작은크기로 자주 불러오거나 처리해야하는경우 사용하면 유용할 것"이라고 한다.

비동기 함수 병렬호출

앞서 설명한 await방식은 결국은 비동기함수를 await키워드당 1회만 불러올 수 있고 맨처음에 말했던 병렬처리는 불가능하다.

하지만 이 방법을 사용하면 비동기 함수를 말그대로 동시에 사용이 가능하다.

활용 예시

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

downloadPhoto()함수는 사진을 다운받는 비동기 함수라고 생각하고(함수 내부 내용은 생각하지 말고) 보자

[]중괄호 안에 호출을 여러개를 넣으면 동시 호출이 가능한데 이는 반쪽짜리 동시호출인게 이대로 호출하면

각 downloadPhoto가 끝나야만 다음 downloadPhoto가 실행되는 구조이다.

동시에 담아 둘수는 있지만 우리가 원했던 형태가 아니다.

다음 형태를 보자

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

다음과 같이 async-let형태로 비동기함수를 호출하고
await[ ]으로 감싸써 비동기 함수들을 호출하면 우리가 원하던 형태의 함수 병렬 호출이 된다.

정리

  1. swift에서 동시에 여러작업을 수행하는 코드를 작성하고자 할때 async-await 방식으로 비동기 함수를 작성하고 호출 할 수 있다

  2. 비동기 데이터를 반복적으로 처리하는 경우 for-await-in방식의 비동기 시퀀스를 이용할 수 있다.

  3. 비동기 함수의 경우 async-let방식을 이용해 병렬처리 즉, 동시에 여러 비동기 함수 호출이 가능하다.

profile
iOS 개발자 취준생, 천 리 길도 한 걸음부터

0개의 댓글