Swift 동시성 시각화 및 최적화 (Visualize and optimize Swift Concurrency)
목차
- Swift Concurrency Recap
- 동시성 최적화 방법
- Concurrency Optimaization
- Thread Pool Exhaustion
- 잠재적 스레드 풀 고갈 및 동시성 코드 오용 문제
- Continuation Misuse
- 이를 방지하는 방법
Swift Concurrency Recap
- Async/await은 블록 단위로 구성되어 thread blocking 없이도 작업을 실행 도중에 일시 중단 했다가 다시 재개할 수 있음.
- Task는 동시성 코드의 기본 작업 단위로 동시성 코드를 실행하고 코드 상태 및 관련 데이터를 관리하는 데 사용.
- Task에는 지역 변수와 취소 처리 비동기 코드의 실행 및 중단 등이 포함.
- 구조화된 동시성 코드를 사용하면 병렬로 실행하고, 완료될 때까지 기다릴 하위 작업을 쉽게 만들 수 있음.
- Swift 언어는 작업을 그룹화하여 사용하지 않을 경우 자동으로 대기 또는 취소하도록 하는 구문을 제공함.
- Actor는 공유 데이터에 접근해야 하는 여러 작업을 조정하고, 외부로부터 데이터를 격리하고 한번에 하나의 작업만 내부 상태를 조작할 수 있도록 하여 데이터 경합에서 안전하게 함.
instrument 14에서는 앱의 모든 활동을 캡처하고 시각화함으로써 앱이 수행하는 작업을 이해하고 문제를 찾고 성능을 개선할 수 있도록 도와줌.
Concurrency Optimization
Swift Concurrency 코드로 앱을 최적화 하는 법을 알아보자.
Swift Concurrency로 올바른 동시성 사용법과 병렬 코드를 쉽게 작성할 수 있지만 동시성 구조를 잘못 사용할 가능성도 있으며, 올바르게 사용했다 하더라도 목표했던 성능상 이점을 얻을 수 없을 수도 있음.
Swift Concurrency를 사용할 때 몇몇 문제는 성능 저하나 버그를 유발하기도 함.
- MainActor blocking은 앱을 정지시킬 수도 있음.
- Actor contention 및 Thread pool exhaustion은 병렬 실행을 줄여 성능을 저하시킴.
- Continuation misuse(연속성 오용)은 누수나 충돌을 유발
instrument를 통해 이를 찾아내고 해결할 수 있음.
MainActor blocking이란 MainActor에서 장시간 동안 사용 작업 실행시 발생
MainActor는 메인 스레드에서 모든 작업을 실행하는 특수한 actor로 UI 작업의 경우 main thread에서 수행되어야 하는데 MainActor는 Swift Concurrency 코드에 통합.
그러나 main thread는 UI에 있어서 매우 중요하기 때문에, 언제나 available을 유지해야 하며 장기 작업을 점유하면 안 됨.
main actor에서 오래 작업하는 경우 앱이 멈추는 것처럼 보여서 길게 동작하는 작업은 백그라운드로 이동시켜야 함.
현재 실행 중인 Task를 표시하며, 동시에 실행하는 작업 수를 보여줌.
두 번째 Task는 특정 시점에 얼마나 많은 작업이 있는지를 나타냄
마지막은 total Task로 해당 시점까지 생성된 작업의 총개수를 그래프로 표시한 것.
앱의 메모리 사용량을 줄이려면 활성화된 Task 및 살아있는 전체 Tsak 통계를 살펴야 함.
코드가 얼마나 잘 병렬화되어 있고, 얼마나 많은 리소스를 소비하는지 알 수 있음.
맨 아래에 있는 Task Forest는 구조화된 동시성 코드 작업 간의 상하위 관계를 나타냄
Task Summray는 각 Task 간에 소요된 시간을 보여줌.
선택한 task에 대해서 모든 정보가 포함된 트랙을 타임라인에 고정할 수 있도록 도와줌
Task를 타임라인에 고정할 경우 4가지 기능이 제공됨.
- 첫째는 Swift의 Task가 어떤 상태인지 보여주는 트랙
- 둘째는 세부 사항을 작업 생성을 역추적하는 기능
- 셋째는 narrative 형태로 Swift Task 대기 상태인 경우 어떤 작업을 기다리고 있는지와 같은 더 많은 맥락 정보를 제공함.
- 넷째는 narrative 형태에서도 동일한 핀 작업에 접근 할 수 있고, 하위 작업이나 child task를 타임라인에 고정할 수 있음.
이는 다른 동시성 기본 요소 및 CPU 관련성을 조사할 때도 도움이 됨.
Concurrency Optimaization 따라 하기
Instruments의 Process 트랙을 보면 UI가 멈춘 원인을 알려줌
여기 상황에서는 Task가 하나만 실행되는데, 이건 모든 작업을 직렬화한 게 문제일 수 있음.
> 따라서 가장 오래 실행 중인 작업을 찾아 해당 작업을 타임라인에 고정
가장 오래 실행되는 트랙의 narrative view를 확인해 볼 수 있음
백그라운드에서 짧게 실행된 후 메인스레드에서 오래 실행된 걸 알 수 있음.
> 메인 스레드를 타임라인에 고정해서 더 자세히 알아보기
메인스레드 고정은 이렇게 함.
메인스레드는 오래 실행되는 여러 작업에 의해 blocking 됨.
이것이 main actor blocking 문제
> 무엇을 실행하고 어디서 왔는지 알아봐야 하는데, narrative view로 돌아가서 답을 찾아볼 수 있음.
세부 사항에서 생성 경로 역추적 정보를 보면 compressAllFiles 함수에서 Task가 생성되었음을 알 수 있음.
Task가 compressAllFiles의 클로저 1을 실행함을 알 수 있음.
해당 심벌을 우클릭하면 source viewer를 열 수 있음.
해당 함수의 클로저 1은 압축 파일을 호출하고 있음.
> 이 Task가 생성된 위치와 수행 중인 작업을 찾았으므로, 메인 스레인에서 해당 작업을 수행하는 걸 피할 수 있음.
compressAllFiles는 CompresstionState안에 위치하고 @MainActor에서 실행되게 되어 있는데, 그렇기에 Task도 메인 스레드에서 실행되었음.
> @Published 속성은 메인스레드에서만 업데이트되어야 하므로 @MainActor에만 있어야 런타임에 문제가 발생하지 않음.
따라서 해당 클래스는 MainActor로 사용하는 대신에 class를 actor로 변경하고, logs는 메인 스레드에서 접근될 필요가 없으므로 코드를 변경
> 이렇게 하면 UI가 block 되지는 않아 지난번보다는 좋아지지만 아직도 더 개선할 포인트가 존재함.
작업 시간을 최소화하려면 머신의 모든 코어를 최대한 활용해야 함.
Actor는 여러 작업에서 접근 가능한 mutable 상태를 안전하게 조작하게 도와주지만, 해당 상태에 대한 접근을 직렬화하는 방식이기 때문에 한 번에 하나의 작업만 actor를 점유할 수 있고, 그동안 다른 작업은 대기해야 함.
만약 여러 작업이 동일한 Actor를 동시에 접근하려고 하면 Actor가 해당 작업의 실행을 직렬화해서 병렬 컴퓨팅이 주는 이점을 누릴 수 없음.
> 왜냐하면 Actor를 사용할 수 있을 때까지 각각의 Task는 대기해야 하기 때문.
이를 해결하기 위해서는 Actor 데이터에 대한 독점 접근 권한이 꼭 필요한 경우에만 Task에서 작업을 실행하고, 그 외에는 모두 Task 밖에서 실행되어야 함.
task를 chunk로 나누어 일부 chunk만 actor에서 실행
task를 chunk로 나누어 일부 chunk만 actor에서 실행하면 컴퓨터가 작업을 더 빨리 끝낼 수 있음.
Task의 Summary를 보면 동시성 코드가 Queue에 작업을 넣은 상태에서 시간을 많이 소요하고 있음
이는 Actor에 접근하는 외부의 많은 Task들이 기다리고 있다는 의미
이때는 작업을 하나를 pin 하여 왜 그런지 알아봄
해당 Task 압축 실행 전 ParallelCompressor Actor에 연결되기를 기다리는 과정에 상당한 시간을 소비함.
해당 Actor를 Timeline에 Pin 해서 보자.
ParallelCompressor Actor에 대해한 최상위 데이터가 존재
이 Actor는 길게 실행되는 작업에 의해서 blocking 된 것처럼 보임.
Task는 실제로 필요한 시간 동안만 actor에 남아 있어야 함.
다시 Narrative로 돌아가면 ParallelCompressor에 Enqueued 후 Task는 클로저 #1에서 실행되므로 다시 이 지점을 조사
소스 코드를 보면 클로저가 압축 작업을 실행.
그런데 거기서 실행하는 compressFile(url: URL) 메서드가 ParallelCompressor Actor 내의 코드라서 다른 작업을 차단하게 됨
이를 해결하기 위해서는 compressFile(url: URL)를 분리된 task로 가져가야 하며, 이를 통해 mutable 상태를 업데이트하는데 필요한 기간도 동안에만 분리된 Task를 수행할 수 있음.
예를 들어서 files 속성에 접근하는 경우 MainActor로 이동하지만, 작업이 완료된 즉시 Thread Pool로 다시 이동함.
logs 속성에 접근해야 할 경우에는 ParallelCompressor Actor로 이동하며, 작업이 종료되는 즉시, Thread Pool로 이동함.
즉, 어떤 스레드에서든 실행될 수 있음.
압축을 해야 하는 건 Thread Pool에서 여러 개이지만, Actor에 제약받지 않음으로 모든 작업을 동시에 실행할 수 있으며, 오직 스레드 수에 의해서만 제한됨.
각 Actor는 한 번에 하나의 Task만 실행할 수 있지만, 대부분읜 경우에 Task가 Actor에 있을 필요가 없으므로 압축 작업을 병렬로 실행하고, 가용 가능한 CPU 코어를 활용할 수 있음.
compressFile 메서드에 nonisolated를 표시하는 것은 해당 Actor의 상태에 대해서접근할 필요가 없다고 Swift 컴파일러에게 알려주는 것이지만, 해당 코드에서는 사용하고 있음.
이 문제를 해결하려면 해당 메서드를 async로 만들 다음, 모든 log 메서드 호출에 await을 명시함.
이제 다음 task 생성을 업데이트해 분리된 task를 만들어야 하는데, Task.detached를 통해 자신이 생성된 Actor Context를 상속하지 않도록 함.
> detached 된 task는 명시적으로 self를 캡처해야 함.
이렇게 할 경우 앱은 모든 파일을 병렬로 업데이트할 수 있으며 UI가 block 되지도 않음.
개선된 결과를 instruments를 통해 확인해 보면 대부분 task는 ParallelCompressor Actor에서 매우 짧은 시간 동안만 실행되었고, queue size는 일정 수준을 넘지 않음.
Thread Pool Exhaustion
잠재적인 성능 문제에는 2가지가 있는데, Thread Pool Exhaustion와 Contunuation Misuse임.
우선 Thread Pool Exhaustion 알아볼 예정.
Thread Pool이 고갈되면 성능이 저하되거나 앱이 deadlock에 빠질 수 있음.
작업 내의 코드가 task안에서 blocking call이 수행되거나, suspending 없이 lock을 획득할 수도 있음.
따라서 Task에서는 blcoking을 호출하지 않아야 하며, 파일 및 네트워크 IO는 비동기 API를 사용하여 수행해야 함.
또한 조건 변수(플래그) 혹은 semaphores를 대기하지 않도록 해야 함.
작은 단위의 짧게 유지되는 Lock은 허용되지만 데이터 경합이 많거나 장기간 유지되는 file/network IO는 피해야 함.
이러한 코드가 있을 경우에는 분리된 영역에서 사용하여 Thread Pool 외부로 이동 Swift Concurrency를 사용하여 연결함.
가능하면 async/await 통해서 시스템이 원활하게 동작하게 하기
Continutation Misuse
Continuation는 다른 형태의 Swift Concurrency 코드를 잇는 단위로, 현재 코드를 잠시 중단하고 다시 재개하는 데 사용.
여기에는 중요한 규칙이 있는데, 정확하게 한 번만 호출해야 함.
콜백 기반 API의 일반적인 요구사항이지만 Swift 언어에서 강제되지 않아서 간과할 수 있음.
콜백이 2번 이상 호출되면 프로그램이 충돌하거나 오동작하고
호출되지 않으면 Task가 누수됨
Continutation 코드에서는 Checked와 Unsafe 2가지 유형이 존재.
성능이 절대적으로 중요한 경우가 아니라면 항상 연속 작업에는 withCheckedContinuation을 사용.
CheckContunuation은 오용을 자동 감지해 오류를 경고로 띄우고
2번 띄우면 트랩 하고, 아예 호출하지 않으면 콘솔에 오류를 보여주고 Instrumnet에서 확인할 수 있음.
(참고)
https://www.youtube.com/watch?v=bq4DVvVJ7H8&t=2s
'apple > WWDC' 카테고리의 다른 글
[WWDC21] Modern Cell configuration (0) | 2025.05.17 |
---|---|
[WWDC22] Meet distributed actors in Swift (0) | 2025.04.30 |
[WWDC21] Protect mutable state with Swift actors (0) | 2025.04.27 |
[WWDC21] Swift Concurrency: Behind the scenes (0) | 2025.04.26 |
[WWDC23] 구조화된 동시성의 기초를 넘어 (Beyond the basics of structured concurrency) (0) | 2025.04.25 |