본문 바로가기

iOS/Swift

Swift 동기(SYNC) 비동기(ASYNC)

스위프트 구조화된 동시성 개요

동시성

  • 동시성(concurrency)은 여러 작업을 병렬로 수행하는 소프트웨어 기능이라고 할 수 있다.
  • Swift는 구조화된 방식으로 비동기(asynchronous)와 병렬(parallel) 코드 작성을 지원한다.
  • 비동기 코드는 일시적으로 중단되었다가 다시 실행할 수 있지만 한 번에 프로그램의 한 부분만 실행한다.
  • 병렬 코드는 동시에 코드의 여러 부분이 실행됨을 의미한다.
  • 동시성이라는 용어를 사용하여 비동기와 병렬 코드의 일반적인 조합을 나타낸다.

스레드(thread)

  • 메인 프로세스 내에서 실행되는 미니 프로세스로 생각할 수 있으며, 그 목적은 애플리케이션 코드 내에서 병렬 실행의 형태를 가능하게 하는 것이다.
  • Swift에서 동시성 모델은 스레드의 최상단에 구축되지만 직접적으로 상호작용할 필요는 없다. (구조화된 동시성이 모든 복잡성을 처리해준다.)

애플리케이션 메인 스레드

  • 앱이 시작될 때 기본적으로 실행되는 단일 스레드를 말하며
  • 주요역할 : UI 레이아웃 렌더링, 이벤트 처리 및 사용자 인터페이스에서 부와 사용자 상호작용 측면에서 사용자 인터페이스를 처리해준다.

동기코드

동기코드는 코드를 보며 설명하겠다.

doSomething1()이라는 함수는 takeTooLong1()이라는 함수와 함께 print()구문을 호출해준다.

takeTooLong1() 함수는 sleep(5)를 호출하여 5초를 지연시키고 print 구문을 호출해준다.

결과를 예상해본다면 print("Start \(Date())") 문이 실행되고 takeTooLong1() 함수가 실행되는 5초동안은

계속 멈춰있는 상태일 것이다. 

// 동기 코드
// takeTooLong() 함수가 실행되는 동안 아무 것도 할 수 없음
func doSomething1() {
	print("Start \(Date())")
	takeTooLong1()
	print("End \(Date())")
}

func takeTooLong1() {
	sleep(5)            // 5초 지연
	print("Async task completed at \(Date())")
}

doSomething1()

결과를 확인해보면

start 다음 5초가 지난 후에 Async 가 출력된 것을 확인할 수 있다.

Start 2023-10-19 13:46:36 +0000
Async task completed at 2023-10-19 13:46:41 +0000
End 2023-10-19 13:46:41 +0000

 

비동기 함수 정의와 호출 (async / await)

  •  함수 파라미터 뒤의 선언부에 async 키워드를 작성해준다.
  •  함수 또는 메서드가 값을 반환한다면 반환 화살표 (->) 전에 async를 작성해준다.
  •  비동기 메서드를 호출할 때 해당 메서드가 반환될 때까지 실행이 일시 중단된다.
  •  중단될 가능성이 있는 지점을 표시하기 위해 호출 앞에 await을 작성해준다.
  •  Task를 이용하여 동기 함수에서 비동기 함수를 호출해준다.

코드를 보고 결과값을 보며 확인해보자

// 비동기 함수 선언 async
// 비동기 함수 호출 await
func doSomething2() async {
	print("Start \(Date())")
	await takeTooLong2()
	print("End \(Date())")
}

func takeTooLong2() async {
	sleep(5)            // 5초 지연
	print("Async task completed at \(Date())")
}

Task {
	await doSomething2()
}

결과값

async 한 메서드를 await 키워드를 사용하여 호출하였지만 아직까지는 동기코드와 동일하게 

5초동안 연산이 멈춰버리고 async한 메서드가 끝난 후 End가 출력된 것을 확인할 수 있다.

Start 2023-10-19 13:52:51 +0000
Async task completed at 2023-10-19 13:52:56 +0000
End 2023-10-19 13:52:56 +0000

 

비동기 함수 병렬로 호출

  • 비동기 함수를 호출하고 주변의 코드와 병렬로 실행하려면 (동시실행)
  • 상수를 정의할 때 let 앞에 async 를 작성하고 상수를 사용할 때마다 await 를 작성해준다.
  • await 를 사용하여 결과를 사용할 수 있을 때까지 실행이 중지된다.

코드와 결과값을 보며 확인해보자

// async-let 바인딩
// 비동기 함수를 호출하고 주변의 코드와 병렬로 실행하려면 (동시실행)
// 상수를 정의할 때 let 앞에 async 를 작성하고, 상수를 사용할 때마다 await 를 작성
func doSomething3() async {
	print("Start \(Date())")
	async let result1 = takeTooLong3()
	async let result2 = takeTooLong3()
	async let result3 = takeTooLong3()
	print("After async-let \(Date())")
	for i in 1...5 { print(i) }
	
	print("result1 End \(await result1)")
	print("result2 End \(await result2)")
	print("result3 End \(await result3)")
	// 비동기 함수와 동시에 실행할 추가 코드
	for i in 6...10 { print(i) }
	
}

func takeTooLong3() async -> Date {
	sleep(UInt32.random(in: 1...5))            // 램덤 초 지연
	return Date()
}

Task {
	await doSomething3()
}

결과값을 확인해보면

async-let 구문을 이용해 async한 함수를 호출해주면 값을 담아주게되는데

async한 메서드 호출 부분을 보면 먼저 호출했다고 하지만 랜덤으로 딜레이를 주면서

호출된 시간이 다르게 나온 것을 볼 수가 있다.

Start 2023-10-19 13:58:49 +0000
After async-let 2023-10-19 13:58:49 +0000
1
2
3
4
5
result1 End 2023-10-19 13:58:54 +0000
result2 End 2023-10-19 13:58:51 +0000
result3 End 2023-10-19 13:58:52 +0000
6
7
8
9
10

 

에러 핸들링과 함께

에러 핸들링은 앞서 작성된 게시물이 있으므로 확인 바란다.

https://minjae1995.tistory.com/26

 

Swift 에러 핸들링

스위프트 코드를 아무리 신중하게 설계하고 구현했다 하더라도 앱을 통제할 수 없는 상황은 반드시 발생한다. 예로 인터넷 연결하여 통신하는 것을 기반으로 동작하는 앱이라면 아이폰의 네트

minjae1995.tistory.com

일단 코드와 결과값을 보며 확인해보자

enum DurationError: Error {
	case tooShort, tooLong
}

// 오류 핸들링과 함께
func doSomething4() async {
		print("Start \(Date())")
		do {
				try await takeTooLong4(delay: 25)
		} catch DurationError.tooShort {
				print("Error: Duration too short")
		} catch DurationError.tooLong {
				print("Error: Duration too long")
		} catch {
				print("Unknown error")
		}

		print("End \(Date())")
}

func takeTooLong4(delay: UInt32) async throws {
		if delay < 5 {
				throw DurationError.tooShort
		} else if delay > 20 {
				throw DurationError.tooLong
		}

		sleep(delay)            // 초 지연
		print("Async task complated at \(Date())")
}

Task {
	await doSomething4()
}

결과값을 보면

takeTooLong4() 함수에서 전달인자로 받아온 UInt32 타입의 값이 

5보다 작으면 tooShort 에러를 던지고

20보다 크면 tooLong 에러를 던지는 것을 볼 수 있다.

난 딜레이로 25초를 주었기에 20보다 큰 것을 확인 후 던져진 Duration too long 에러를 잡아

print한 것을 확인할 수 있다.

역시나 메서드가 다 실행된 후 End가 프린트되었다.

비동기 코드에서 열거형 타입으로 선언된 에러를 어떤식으로 핸들링할 수 있는 지

확인할 수 있었던 코드다.

Start 2023-10-19 14:13:36 +0000
Error: Duration too long
End 2023-10-19 14:13:36 +0000

 

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

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

  • 모든 비동기 코드는 어떠한 작업의 일부로 실행한다
  • 작업은 계층 구조로 정렬이 가능하다
  • 작업 그룹 (task group) 을 생성하고 해당 그룹에 하위 작업을 추가가 가능하다
  • 작업과 작업 그룹 간의 명시적 관계 때문에 이 접근 방식을 구조적 동시성 또는 구조화된 동시성 이라고 한다.

작업 그룹

  • 동적인 조건에 따라 여러 작업을 동시에 생성하고 실행해야 하는 상황에서 사용하며
  • withTaskGroup() 메서드를 사용한다.
  • addTask() 메서드를 호출하여 각각의 새로운 작업을 추가한다.
  • cancelAll() 메서드는 그룹의 모든 작업을 취하는 메서드
  • isCancelled 작업 그룹이 이미 취소되었는지 여부를 확인하는 프로퍼티이고
  • IsEmpty 작업 그룹 내에 작업이 남아 있는지 여부를 확인하는 프로퍼티이다.
  • 데이터 경쟁 피하기
  • 동시에 데이터에 접근하는 여러 작업은 데이터 경쟁(Data Race) 조건이 발생할 위험이 있으며
  • 여러 작업이 동시에 동일한 데이터에 접근하려고 시도하면 데이터 오류가 발생할 수 있다.

코드를 보며 확인해보자

https://developer.apple.com/documentation/swift/withtaskgroup(of:returning:body:)

 

withTaskGroup(of:returning:body:) | Apple Developer Documentation

Starts a new scope that can contain a dynamic number of child tasks.

developer.apple.com

withTaskGroup 메서드를 애플 공식사이트에서 확인해보면

동적인 상황에서 작업단위로 작업을 해야할 때 사용하는 메서드?라고 나와있는 것 같다.

of: 라는 매개변수에 ChildTaskResult.Type 즉 여러 태스크의 단위를 타입으로 정해달라는 의미이다.

난 (Int, Date)인 튜플 타입으로 Task의 타입을 정해줬고

5번 반복을 해주며 해당 인덱스와 async한 메서드를 호출 후 반환값을 함께 group.addTask 메서드를 통해  group에 담아주었다.

그 후 group의 요소들을 순회하며 딕셔너리 형태의 변수에 담아주었다.

func doSomething5() async {
	var timeStamps: [Int:Date] = [:]
	
	print("Start \(Date())")
	
	// 반환 타입을 변경 : Void.selt -> (Int, Date)
	await withTaskGroup(of: (Int, Date).self) { group in
		for i in 1...5 {
			group.addTask {
				return (i, await takeTooLong5())
			}
		}
		
		// for-await 표현식을 사용하여 비동기적으로 반환되는 일련의 값을 루프
		// 동시 작업에서 반환되는 값의 수신을 기다려서 처리
		// 일련의 데이터가 AsyncSequence 프로토콜 준수가 필수 요구사항
		for await (task, data) in group {
			timeStamps[task] = data
		}
	}
	
	// 작업 그룹이 종료된 후 저장된 timeStamps 딕셔너리 항목을 출력
	for (task, data) in timeStamps {
		print("Task = \(task), Date = \(data)")
	}
	
	print("End \(Date())")
}

func takeTooLong5() async -> Date {
	sleep(5)            // 5초 지연
	return Date()
}

Task {
	await doSomething5()
}

 

위 코드의 결과값을 확인해보면 이렇다.

Start 2023-10-19 14:21:41 +0000
Task = 1, Date = 2023-10-19 14:21:46 +0000
Task = 3, Date = 2023-10-19 14:21:46 +0000
Task = 5, Date = 2023-10-19 14:21:46 +0000
Task = 4, Date = 2023-10-19 14:21:46 +0000
Task = 2, Date = 2023-10-19 14:21:46 +0000
End 2023-10-19 14:21:46 +0000

 

동시성 예제

1부터 100까지의 합을 구하는 비동기 함수

 async let을 사용하여 두 개의 비동기 작업을 동시에 실행하고 결과를 합하여 출력하는 함수를 작성해보세요.

 

 1...100 더하는 함수

 

 1...50 더하는 함수 1개 작업을 실행

 51...100 더하는 함수 1개 작업을 실행

 

 두 결과를 더해서 1...100 합한 값을 출력하세요.

 

라는 예제를 위의 코드들을 활용하여 풀어보았다.

func sum(_ start: Int, _ end: Int) async -> Int {
	async let a = sumFrom(start, end / 2)
	async let b = sumFrom(end / 2 + 1, end)
	return (await a) + (await b)
}

func sumFrom(_ start: Int, _ end: Int) async -> Int {
	return (start...end).reduce(0, +)
}

Task {
	print(await sum(1, 100))   // 5050?
}
func sum2(_ start: Int, _ end: Int) async -> Int {
	var totalSum: [Int: Int] = [:]
	let indexlist = [start, end]
	await withTaskGroup(of: (Int, Int).self) { group in
		for i in 0...1 {
			group.addTask {
				if i == 1 {
					return (i, await sumFrom(indexlist[0], indexlist[1] / 2))
				}
				return (i, await sumFrom(indexlist[1] / 2 + 1, indexlist[1]))
			}
			
			for await (task, data) in group {
				totalSum[task] = data
			}
		}
	}
	return totalSum.values.reduce(0, +)}

Task {
	print(await sum2(1, 100))   // 5050?
}

에러 핸들링과 함께하는 동시성 예제

enum InputNumberError: Error {
	case bothInputZero, secondInputZero, sameInput ,lessThanSecond
}

func sum3(_ start: Int, _ end: Int) async throws -> Int {
	guard end != 0 else { throw InputNumberError.secondInputZero }
	guard start != 0 && end != 0 else { throw InputNumberError.bothInputZero }
	guard start == end else { throw InputNumberError.sameInput }
	guard start < end else { throw InputNumberError.lessThanSecond }
	
	var totalSum: [Int: Int] = [:]
	let indexlist = [start, end]
	await withTaskGroup(of: (Int, Int).self) { group in
		for i in 0...1 {
			group.addTask {
				if i == 1 {
					return (i, await sumFrom(indexlist[0], indexlist[1] / 2))
				}
				return (i, await sumFrom(indexlist[1] / 2 + 1, indexlist[1]))
			}
			
			for await (task, data) in group {
				totalSum[task] = data
			}
		}
	}
	return totalSum.values.reduce(0, +)
}

func sumFrom(_ start: Int, _ end: Int) async -> Int {
	return (start...end).reduce(0, +)
}

Task {
	do {
		print(try await sum3(1, 100))   // 5050?
	} catch InputNumberError.bothInputZero {
		print("ERROR :: bothInputZero")
	} catch InputNumberError.secondInputZero {
		print("ERROR :: secondInputZero")
	} catch InputNumberError.sameInput {
		print("ERROR :: sameInput")
	} catch InputNumberError.lessThanSecond {
		print("ERROR :: lessThanSecond")
	} catch {
		print("ERROR :: Unknown error")
	}
}

'iOS > Swift' 카테고리의 다른 글

Swift 서브 스크립트 (Subscript)  (0) 2023.11.07
Swift 액터(Actor)  (2) 2023.10.23
Swift 에러 핸들링  (1) 2023.10.18
Swift 열거형(enumeration)  (0) 2023.10.18
Swift stride(from:to:by:)  (1) 2023.09.23