🧢
동기 vs 비동기 프로그래밍
June 26, 2024
Contents
-
프로세스와 스레드
-
동기 vs 비동기 프로그래밍
-
비동기 프로그래밍 구현 방법
- GCD (Grand Central Dispatch)
- async await
-
async await 를 사용할 때 주의점
프로세스와 스레드, 교착상태 이해하기
라면을 끓이는 과정으로 비유해서 이해할 수 있다.
프로세스와 스레드
-
CPU = 요리사:
- 요리사는 CPU처럼 주방에서 여러 요리를 처리하는 사람입니다.
-
프로세스 = 라면을 끓이는 과정:
- 라면을 끓이는 과정이 하나의 독립된 작업, 즉 프로세스입니다. 각 라면 요리는 독립적으로 수행되며, 다른 요리(프로세스)와 직접적으로 영향을 주지 않습니다.
-
스레드 = 작은 업무:
- 스레드는 프로세스 내에서 작은 작업들입니다. 예를 들어, 라면을 끓이는 과정에서 면을 끓이고, 국물을 만드는 것이 각각의 스레드라고 할 수 있습니다.
- 또 다른 예로, 게임을 다운받는 동안 브라우저의 다른 탭에서 작업하는 것이 스레드입니다.
- 이때, 스레드는 ‘주방 칼’처럼 하나의 자원을 공유하며 작업을 수행합니다.
교착 상태(데드락) 비유
-
파를 써는 과정 = 스레드:
- 요리사가 파를 써는 과정은 하나의 스레드입니다. 이 스레드는 칼이라는 자원을 사용합니다.
-
양파를 써는 과정:
- 동일한 스레드에서 파를 쓰는 동안 양파를 썰어야 하는 경우를 생각해봅시다. 파를 써는 작업이 끝나지 않았지만 양파를 쓸기 위해 칼(자원)이 필요합니다. 그러나 칼이 이미 파를 써는 데 사용되고 있기 때문에, 양파를 쓸 수 없습니다.
-
교착 상태(데드락):
- 이 상황이 교착 상태입니다. 파를 써는 스레드가 칼을 계속 사용하고 있는 동안, 동일한 칼이 필요하지만 사용할 수 없는 상태가 됩니다. 두 작업 모두 완료되지 못하고 멈추게 됩니다.
식사하는 철학자 문제
- 식사하는 철학자 문제는 컴퓨터 과학에서 교착 상태와 자원 할당 문제를 설명하는 전형적인 문제입니다. 철학자들이 식사하고 생각하는 행위를 반복하며, 식사하기 위해 포크 두 개가 필요합니다. 하지만 포크는 공유 자원이기 때문에 철학자들이 동시에 포크를 잡으려 하면 교착 상태가 발생할 수 있습니다.
요약
- 프로세스: 독립적인 작업(라면을 끓이는 과정)이며, 각 프로세스는 독립된 자원과 메모리를 가집니다.
- 스레드: 프로세스 내에서 작은 작업들(파를 써는 과정)이며, 자원을 공유합니다.
- 교착 상태: 자원을 공유하는 스레드가 서로 필요한 자원을 점유하고 있어 작업이 진행되지 않는 상태(파를 써는 동안 양파를 써야 하는 경우).
- 식사하는 철학자 문제: 교착 상태와 자원 할당 문제를 설명하는 전형적인 예.
이 비유를 통해 프로세스, 스레드, 그리고 교착 상태에 대한 개념을 더 명확하게 이해할 수 있습니다.
동기와 비동기 프로그래밍
동기 프로그래밍
- 정의: 작업이 순차적으로 실행되는 방식.
- 특징:
- 하나의 작업이 완료될 때까지 다음 작업이 시작되지 않음.
- 코드가 직관적이고 이해하기 쉬움.
- 긴 작업이 있을 경우, 메인 스레드를 블로킹할 수 있음.
비동기 프로그래밍
- 정의: 작업이 동시에 실행될 수 있는 방식.
- 특징:
- 작업이 완료되기를 기다리지 않고 다음 작업을 시작할 수 있음.
- 메인 스레드의 블로킹을 방지하여 응답성을 향상시킴.
- 비동기 작업의 결과는 콜백, 프로미스, async/await 등을 통해 처리.
비동기 프로그래밍의 구현 방법
GCD (Grand Central Dispatch)
- 정의: Apple이 제공하는 라이브러리로, 비동기 작업을 큐에 추가하여 병렬로 실행할 수 있음.
- 특징:
- 글로벌 큐와 메인 큐를 사용하여 작업을 관리.
- 작업의 우선순위를 설정할 수 있음.
- 문제점:
- 콜백 지옥(callback hell) 문제가 발생할 수 있음.
- 코드가 복잡하고 가독성이 떨어짐.
예제 코드 (GCD 사용)
func fetchDataFromServer(completion: @escaping (Data?) -> Void) {
DispatchQueue.global().async {
sleep(2) // 네트워크 요청 시뮬레이션
let data = "Server Data".data(using: .utf8)
completion(data)
}
}
fetchDataFromServer { data in
// 데이터를 처리하는 코드
}
async와 await
정의
- async: 비동기 함수임을 나타냄.
- await: 비동기 함수의 완료를 기다림.
특징
- 비동기 코드를 동기 코드처럼 작성할 수 있어 가독성이 향상됨.
- 비동기 작업을 순차적으로 실행할 수 있음.
- 에러 처리가 간편해짐.
예제 코드 (async/await)
import Foundation
func fetchDataFromServer() async throws -> Data {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 네트워크 요청 시뮬레이션
guard let data = "Server Data".data(using: .utf8) else {
throw NSError(domain: "Invalid Data", code: 1, userInfo: nil)
}
return data
}
Task {
do {
let data = try await fetchDataFromServer()
print("Fetched Data: \(data)")
} catch {
print("Failed to fetch data: \(error)")
}
}
async let
async let을 사용하면 비동기 작업을 동시에 시작하고 그 결과를 나중에 사용할 수 있습니다. 이를 통해 동시성을 쉽게 구현할 수 있습니다.
import Foundation
// Helper function to simulate network fetch
func fetchPage(url: String) async -> String {
print("Fetching \(url)...")
try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) // Simulate network delay
return "Content of \(url)"
}
// Main function to fetch multiple pages concurrently
func fetchMultiplePages() async {
// Start fetching pages concurrently
async let page1 = fetchPage(url: "https://example.com/page1")
async let page2 = fetchPage(url: "https://example.com/page2")
async let page3 = fetchPage(url: "https://example.com/page3")
// Await the results
let result1 = await page1
let result2 = await page2
let result3 = await page3
// Print the results
print("Result 1: \(result1)")
print("Result 2: \(result2)")
print("Result 3: \(result3)")
}
// Execute the function
Task {
await fetchMultiplePages()
}
async await 를 사용할 때 주의점(스레드 관리)
아래 글을 참조
async, await 를 사용하면서 겪은 UI thread(Main Thread) 관리 문제
결론
- SwiftUI는 UI에서 비동기 코드를 호출하는 것을 최대한 자연스럽게 만드는 메커니즘을 제공한다.
- .task 뷰 수정자를 사용하여 뷰가 나타날 때 비동기 코드를 실행할 수 있으며, 뷰가 사라지면 SwiftUI가
자동으로 작업을 취소한다.
- .refreshable과 .searchable 뷰 수정자는 클로저에 비동기 컨텍스트를 생성하므로 내부에서 쉽게 비동기 코드를 호출할 수 있다.