티스토리 뷰

728x90
반응형

Swift concurrency: Behind the scenes  영상을 보고 정리해놓은 글입니다.

내용 및 사진 출처는 아래 링크를 참고해 주세요 😊

(오타정정 및 피드백 대환영🙌🏻)

 

https://developer.apple.com/videos/play/wwdc2021/10254/

 

Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer

Dive into the details of Swift concurrency and discover how Swift provides greater safety from data races and thread explosion while...

developer.apple.com


📚 영상을 볼 때 필요한 사전 지식

1. async/await

2. structured concurrency

3. actor

 

🎬 추가로 참고하면 좋을 영상

1. Swift Concurrency 관련 영상

- Meet async/await in Swift

- Explore structured concurrency in Swift

- Protect mutable state with Swift actors

2. Thread explosion 관련 영상

- Building Responsive and Efficient Apps with GCD

- Concurrent Programming With GCD in Swift 3

- Modernizing Grand Central Dispatch Usage

 

💡 영상의 목적

1. Swift Concurrency가 성능과 효율성을 위해 설계된 이유을 더 깊이 알아보고자 함

2. Swift Concurrency에 대해 추론하는 방법을 알아보고자 함

3. Grand Central Dispatch와 같은 기존 스레딩 라이브러리와 상호 작용하는 방법에 대한 더 나은 정신적 모델을 제공하고자 함


🔥 내용 정리

1.  Threading Model

ex) 뉴스 피드 리더 앱 예시 - GCD 사용

01234

 구성요소 

1️⃣ 사용자 인터페이스:  Main Thread

2️⃣ 데이터베이스: 사용자가 구독하는 뉴스 피드 추적용

3️⃣ 네트워킹: 피드에서 최신 콘텐츠 가져옴

 

 사용자가 새 뉴스 피드를 요청했을 경우 

 1️⃣ Main Thread 에서 User envent gesuture 처리하여,
Database 처리 Serial Queue에게 비동기 요청함

더보기

<Serial Queue에게 비동기 요청을 한 이유>

1. 작업을 다른 대기열로 디스패치하여 Main Thread가 잠재적으로 많은 양의 작업이 발생할 때까지 기다리는 동안에도 사용자 입력에 계속 응답할 수 있도록 하려고
2. Serial Queue 상호 배제를 보장하므로 데이터베이스에 대한 액세스가 보호되기 때문에

상호 배재(mutual exclusion)
- 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 임계 구역에 들어올 수 없는 것
(강민철, 『혼자 공부하는 컴퓨터 구조+운영체제』, 한빛미디어(2022), p348)

 

2️⃣ Database 대기열에 있는 동안 사용자가 구독한 뉴스 피드에 대해 비동기 요청을 반복하고,

각 피드마다 URLSession에 대한 Networking 요청을 예약함

 

3️⃣ Networking 요청의 결과가 들어오면,

Concurrent Queue인 Delegate Queue에서 URLSession 콜백이 호출됨

 

4️⃣ 각 결과에 대한 completion handler에서 최신 요청으로 데이터베이스를 동기식(synchronously)으로 업데이트하고,

나중에 사용할 수 있도록 캐시 처리함

 

5️⃣ Main Thread를 깨워 UI를 새로 고침함

 

 

 GCD사용 시 CPU 상황 (GCD Threading Model) 

| 위에 코드(슬라이드 다섯번째 사진 참고)가 어떻게 작동되는지 살펴보자

 

1️⃣ GCD는 피드 업데이트 결과를 처리하기 위해 먼저 두 개의 스레드를 가져옴

- 위 아래 CPU에서 각각 스레드 가져옴


2️⃣ 스레드가 데이터베이스 큐 액세스를 차단하면  네트워킹 큐에서 계속 작업하기 위해 더 많은 스레드가 생성됨

 

3️⃣ 네트워킹 결과를 처리하는 여러 스레드 간에 문맥 교환 수행

- 흰색 선마다 문맥 교환 이뤄짐

더보기

문맥 교환(context switch)
- 하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 
  이전의 프로세스의 상태(문맥)를 보관하고 새로운 프로세스의 상태를 적재하는 작업
  (출처: 위키백과)

 

 위 상황에서 문제점  

- 스레드가 차단 될때마다 새로운 스레드를 가져옴

- CPU 코어보다 더 많은 스레드로 오버커밋 될 수 있음 (스레드가 과도하게 많아짐)

➡️ 스레드 폭발(Thread explosion) 발생

 

 Swift Concurrency가 나오게 된 배경   

01

- 스레드 폭발의 결과로 메모리 및 스케줄링 오버헤드가 발생할 수 있음

- 이러한 결과가 CPU 실행 효율성을 떨어뜨림

➡️ Swift Concurrency에서는 성능과 효율성을 염두하여,

앱이 제어되고 구조화되고, 안전하게 Concurrency가 될 수 있도록 구축함

 

 Swift Concurrency 사용 시 CPU 상황 ( Swift Concurrency Threading Model) 

- 문맥 교환 대신 함수 호출 비용만 지불함

 

 

2.  Runtime 계약 유지

 Runtime 계약 유지가 필요한 이유 

-  Swift Concurrency를 사용하여 CPU 코어 수만큼만 스레드를 생성하고

스레드가 차단될 때 작업 항목 간에 저렴하고 효율적으로 전환할 수 있도록 하려면,

운영 체제에는 스레드가 차단되지 않는 Runtime 계약이 필요함

 

 

 Runtime 계약 유지를 할 수 있게 해주는 언어 기능 

1️⃣ await and non-blocking of threads

- await는 비동기 대기 (“Meet async/await in Swift” 참고)
- async 함수의 결과를 기다리는 동안 현재 스레드를 차단하지 않음
- 대신 함수가 일시 중단되고 스레드가 해제되어 다른 작업을 실행할 수 있음

 

 

2️⃣ tracking of dependencies in swift task model

- URLSession의 await 이후로 실행되는 코드가 존재

- `articles` 상수에 값이 할당되고, 할당된 값이 `updateDatabase`의 인자값이 되려면  

URLSession의 결과로 data 상수의 값이 할당되야함

- 이렇듯, 비동기 함수가 완료된 후에만 실행될 수 있는 것을 `Continuation`이라고 함

- 이것이 Swift Concurrency Runtime에 의해 추적되는 종속성

 

- 또한 작업 그룹 내에서 부모 Task는 여러 자식 Task를 만들 수 있으며 각 자식 Task를 완료해야 부모 Task를 진행할 수 있음

- 이것은 Task 그룹의 범위에 의해 코드에서 표현됨

- 따라서 Swift 컴파일러 및 런타임에 명시적으로 알려진 종속성임
- 단, Swift에서 Tasks는 Swift Runtime에 알려진 다른 Task(연속 또는 자식 Task)만 기다릴 수 있음
- 따라서 Swift Concurrency primitives로 구조화된 코드는 Task 간의 종속성 체인에 대한 명확한 이해를 런타임에 제공함

 

3.  Swift Concurrency 채택 시 고려 사항

1️⃣ Concurrency를 도입하는 비용이 관리 비용보다 클 때만 Swift Concurrency를 작성하도록 주의해야 함

-  Swift Concurrency를 채택할 때 코드의 성능 특성을 이해하기 위해 Instruments 시스템 추적으로 코드를 프로파일링 하는 것이 좋음

좋지 않은 예시

2️⃣  await and atomicity 관련 주의사항

 

01

- Swift는 await 이전에 코드를 실행한 스레드가 연속 작업을 선택할 스레드와 동일하다는 것을 보장하지 않음
- 실제로 await은 Task가 자발적으로 취소될 수 있기 때문에 원자성이 손상되었음을 나타내는 코드의 명시적 지점임

- await에서 잠금을 유지하지 않도록 주의해야 함

- 마찬가지로 스레드 관련 데이터는 대기 중에도 보존되지 않음
- 스레드지역성을 예상하는 코드의 모든 가정은 await의 일시 중단 동작을 설명하기 위해 다시 검토해야 함

 

3️⃣ Runtime 계약 관련 주의사항

- Swift Concurrency를 채택할 때 협력 스레드 풀이 최적으로 작동할 수 있도록 코드에서 이 계약을 계속 유지하는지 확인하는 것이 중요함

- os_unfair_locks 및 NSLocks와 같은 primitives도 안전하지만 사용 시 주의가 필요

- 세마포어 및 조건 변수와 같은 primitives는 Swift Concurrency과 함께 사용하기에 안전하지 않음

 

-  구조화되지 않은 Task를 생성한 다음 세마포어 또는 안전하지 않은 기본 요소를 사용하여 작업 경계를 넘어 종속성을 소급 도입하는 기본 요소를 사용하지 말아야 함

- 위 코드는 스레드가 다른 스레드가 차단을 해제할 수 있을 때까지 세마포어에 대한 무기한 차단을 할 수 있음

- 이것은 스레드에 대한 진행률의 런타임 계약 위반임

 

- 안전하지 않은 primitives의 사용을 식별하는데 도움이 되도록 환경 변수로 앱 테스트하는 것을 추천

- 수정된 디버그 런타임에서 앱이 실행되어 순방향 진행의 불변성을 적용함

 

 

4.  Actor

- 상호 배제 보장 ➡️ 동시에 액세스 되지 않음 ➡️ data race 방지

- 한 번에 최대 하나의 메서드 호출을 실행할 수 있음

 

 Actor와 다른 형태의 상호 배제 

 

1️⃣ Serial Queue sync / Locks

- 대기열이 아직 실행 중이 아니면 경합이 없다고 말함
- 이 경우 호출 스레드는 문맥 전환 없이 큐에서 새 작업 항목을 실행하는 데 재사용됨
- 대신 직렬 대기열이 이미 실행 중인 경우 대기열이 경합 상태에 있다고 함
- 이 상황에서는 호출 스레드가 차단 ➡️ 스레드 폭발 가능성 높임

 

2️⃣ Serial Queue async
- 차단과 관련된 문제 때문에 일반적으로 dispatch async 사용을 선호하는 것이 좋음
- dispatch async의 주요 이점은 차단되지 않는다는 것  
- 따라서 경쟁 상황에서도 스레드 폭증으로 이어지지 않음
- Serial Queue과 함께 dispatch async를 사용하는 경우의 단점은 경합이 없을 때 호출 스레드가 다른 작업을 계속하는 동안 Dispatch가 비동기 작업을 수행하기 위해 새 스레드를 요청해야 한다는 것
- 따라서 dispatch async를 자주 사용하면 과도한 Thread wakeups 및 문맥 전환이 발생할 수 있음

 

3️⃣ Actor
- Actor는 효율적인 스케줄링을 위해 협력 스레드 풀을 활용하여 위 두 개의 장점을 결합함
- 실행 중이 아닌 Actor에서 메서드를 호출하면 호출 스레드를 재사용하여 메서드 호출을 실행할 수 있음
- 호출된 Actor가 이미 실행 중인 경우 호출 스레드는 실행 중인 함수를 일시 중단하고 다른 작업을 선택할 수 있음

 

 Actor hopping 

- Actor는  협력 스레드 풀에서 실행됨

- 이 스레드 풀에서 한 Actor에서 다른 Actor로 실행 전환이 되는 경우를 Actor hopping이라고 함

- 즉,  Actor에 대한 작업을 일시 중단하고 새  Actor에 대한 작업을 실행하는 경우가 Actor hopping

 

 Actor reentrancy (Actor 재진입) 

- Actor 재진입에 대한 지원은 Actor가 엄격하게 선입 선출이 아닌 순서로 항목을 실행할 수 있음을 의미

- Actor는 재진입을 위해 설계되었음

- 그래서 런타임은 우선순위가 높은 항목을 대기열의 맨 앞으로, 우선 순위가 낮은 항목보다 먼저 이동하도록 선택할 수 있음
- 이렇게 하면 우선 순위가 높은 작업을 먼저 실행하고 우선 순위가 낮은 작업을 나중에 실행할 수 있음
- 이는 우선 순위 역전 문제를 직접 해결하여 보다 효과적인 스케줄링 및 리소스 활용을 허용함

 

 

 MainActor 

- MainActor는 Main Thread를 추상화한 것

- 사용자 인터페이스를 업데이트할 때 MainActor를 호출하거나 MainActor에서 호출해야 함

- Main Thread는 협력 스레드 풀과 분리되어 있으므로 문맥 전환이 필요함

- 때문에 협력 스레드 풀과 Main Thread 사이에 문맥 전환이 자주 발생하는 경우 오버 헤드가 생길 수 있음

- 이를 방지하기 위해 각 스레드에서 해야 하는 작업을 일괄 처리되도록 코드를 재구성해야 함

728x90
반응형
댓글