스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift 동시성 시각화 및 최적화
Instruments에서 Swift 동시성 템플릿을 통해 앱을 최적화하는 방법을 알아보세요. 흔히 발생하는 성능 문제를 논의하고 Instruments를 사용하여 이러한 문제를 찾고 해결하는 방법을 보여드리겠습니다. UI의 응답성을 유지하고 병렬 성능을 최대화하며, 앱 내에서 Swift 동시성 활동을 분석하는 방법을 알아보세요. 이 세션을 최대한 활용하려면 Swift 동시성(Task 및 Actor 포함)에 대해 숙지하시는 것이 좋습니다.
리소스
관련 비디오
WWDC23
WWDC22
WWDC21
-
다운로드
♪ ♪
Swift 동시성의 시각화 및 최적화에 오신 것을 환영합니다 Swift 런타임 라이브러리 담당인 Mike라고 합니다 안녕하세요, Instruments 를 담당하고 있는 Harjas입니다 저희는 Swift 동시성 코드를 더 잘 이해하고 보다 빠르게 만드는 방법과 Instruments 14의 새로운 시각화 도구에 대해 말씀드리고자 합니다 먼저 이해를 돕기 위해 Swift 동시성의 다양한 부분과 그 연동 방식에 대해 간략하게 설명해드릴 거예요 그런 다음 새로운 동시성 Instruments 시연을 통해 Swift 동시성 코드를 사용하는 앱의 실제 성능 문제를 해결하는 방법을 보여드리고 마지막으로 잠재적인 스레드 풀 고갈 및 연속성 코드 오용 문제와 이를 방지하는 방법에 대해 알아보겠습니다 작년에 선보인 Swift 동시성 코드는 새로운 언어 기능으로 async 및 await 패턴과 구조화된 동시성 그리고 행위자를 포함하는데요 기쁘게도 도입 이후 Apple 안팎에서 이러한 기능이 광범위하게 채택되었습니다 Swift 동시성 코드가 제공하는 새로운 연동 기능들은 동시성 프로그래밍을 보다 쉽고 안전하게 만들어주죠 Async/await는 동시성 코드의 기본 구문 빌딩 블록으로 실행 스레드 블로킹 없이도 작업을 실행 도중에 일시 중단했다가 나중에 재개할 수 있는 함수를 생성하고 호출할 수 있게 해줍니다
태스크는 동시성 코드의 기본 작업 단위로 동시성 코드를 실행하고 코드 상태 및 관련 데이터를 관리하는데 작업에는 지역 변수와 취소 처리 비동기 코드의 실행 및 중단 등이 포함되죠 구조화된 동시성 코드를 사용하면 병렬로 실행하고 완료될 때까지 기다릴 하위 작업을 쉽게 만들 수 있습니다 Swift 언어는 작업을 그룹화하여 사용하지 않을 경우 자동으로 대기 또는 취소하도록 하는 구문을 제공하죠 행위자는 공유 데이터에 접근해야 하는 여러 작업을 조정합니다 외부로부터 데이터를 격리하고 한 번에 하나의 작업만 내부 상태를 조작할 수 있도록 하여 동시성 변이로 인한 데이터 레이스를 방지하죠 Instruments 14에 새로 도입된 도구 세트는 앱의 모든 활동을 캡처하고 시각화함으로써 앱이 수행하는 작업을 이해하고 문제를 찾고 성능을 개선할 수 있도록 해줍니다 Swift 동시성 코드의 기본 기능에 대한 보다 자세한 내용은 관련 영상 섹션에 링크된 영상을 통해 확인하실 수 있어요
Swift 동시성 코드로 앱을 최적화하는 방법을 살펴보죠 Swift 동시성으로 올바른 동시성 및 병렬 코드를 쉽게 작성할 수 있지만 동시성 구조를 잘못 사용할 가능성도 있으며 올바르게 사용했더라도 목표했던 성능상의 이점을 얻을 수 없는 방식일 수도 있죠
Swift 동시성을 사용하여 코드 작성 시 흔히 발생하는 몇몇 문제들은 성능 저하나 버그를 유발합니다 메인 행위자 블로킹은 앱을 정지시킬 수 있으며 행위자 경합 및 스레드 풀 고갈은 병렬 실행을 줄여 성능을 저하시키며 연속성 코드 오용은 누수나 충돌을 유발하는데 새로운 Swift 동시성 도구로 이런 문제를 찾아내고 해결할 수 있죠 그럼 메인 행위자 블로킹부터 하나씩 살펴볼게요 메인 행위자 블로킹은 메인 행위자에서 장시간 동안 작업 실행 시 발생합니다 메인 행위자는 메인 스레드에서 모든 작업을 실행하는 특수한 행위자로 UI 작업의 경우 메인 스레드에서 수행되어야 하는데 메인 행위자는 UI 코드를 Swift 동시성 코드에 통합해주죠 그러나 메인 스레드는 UI에 있어서 매우 중요하기 때문에 가용성을 유지해야 하므로 장기 실행 작업으로 점유해서는 안 되며 그럴 경우 앱이 잠긴 것처럼 보이고 응답하지 않게 됩니다 메인 행위자에서 실행되는 코드는 신속하게 작업을 완료하거나 계산을 메인 행위자에서 백그라운드로 이동시켜야 하는데 작업은 일반 액터나 별도의 작업에 배치하여 백그라운드로 이동시킬 수 있으며 메인 행위자에서 작은 단위 작업을 실행하여 UI를 업데이트하거나 메인 스레드에서 해야 하는 다른 작업을 수행할 수 있어요 실제 작동하는 모습을 보여드리죠 고마워요, Mike 여기에 File Squeezer라는 앱이 있는데요 저희가 폴더 내의 모든 파일을 빠르게 압축하기 위해 만든 것으로 작은 파일은 문제 없이 압축되지만 대용량 파일의 경우 예상보다 훨씬 오래 걸리는 데다가 UI가 완전히 정지되어 어떠한 동작에도 반응하지 않습니다 이 경우 사용자는 당혹감을 느끼며 앱이 고장났거나 종료되지 않을 것이라고 생각할 수 있는데요 최고의 사용자 경험을 위해서는 UI가 항상 반응할 수 있도록 만들어야 하죠 Instruments의 새로운 Swift 동시성 도구를 사용하면 이러한 성능 문제를 조사할 수 있습니다 Swift 태스크 및 행위자 도구는 동시성 코드를 시각화하고 최적화할 수 있는 완전한 도구 모음을 제공하죠 성능 문제 조사에서 가장 먼저 할 일은 Swift 태스크 도구에서 제공하는 최상위 통계를 살펴보는 겁니다 그 중 첫 번째는 '실행 중인 태스크'로 동시에 실행 중인 작업의 수를 보여주죠 다음에 있는 '활성 태스크'는 특정 시점에 얼마나 많은 작업이 있는지를 나타내며 마지막으로 '총 태스크'는 해당 시점까지 생성된 작업의 총 개수를 그래프로 표시한 겁니다 앱의 메모리 사용량을 줄이려면 활성 태스크 및 총 태스크 통계를 자세히 살펴봐야 하며 이러한 모든 통계를 조합하면 코드가 얼마나 잘 병렬화되어 있고 얼마나 많은 리소스를 소비하는지 알 수 있어요 화면 아래쪽에 있는 태스크 포레스트는 Instruments의 많은 세부 보기 항목 중 하나로 구조화된 동시성 코드 작업 간의 상하위 관계를 그래픽으로 표현한 것입니다 다음으로 작업 요약 보기는 다양한 상태에서 각 태스크에 소요된 시간을 보여주는 것으로 마우스 오른쪽 버튼을 클릭하면 선택한 작업에 대한 모든 정보가 포함된 트랙을 타임라인에 고정할 수 있도록 보기 기능이 강화되었으며 이를 통해 장기간 실행 중이거나 행위자에 대한 접근 권한을 얻기 위해 대기 중인 관심 작업을 빠르게 찾고 확인할 수 있어요 Swift 태스크를 타임라인에 고정하면 4가지 주요 기능이 제공되는데 첫 번째는 Swift 태스크가 어떤 상태인지 보여주는 트랙이고 두 번째는 세부 사항 보기에서 작업 생성을 역추적하는 기능 세 번째는 내러티브 보기를 통해 현재 Swift 태스크가 대기 중 상태인 경우 어떤 작업을 기다리고 있는지와 같은 더 많은 맥락 정보를 제공하죠 마지막으로 내러티브 보기에서도 요약 보기에서와 동일한 핀 작업에 접근할 수 있으므로 하위 작업이나 스레드 또는 Swift 행위자를 타임라인에 고정할 수 있습니다 이러한 내러티브 보기는 Swift 태스크와 다른 동시성 기본 요소 및 CPU의 관련성을 찾을 때도 도움이 되죠 이제 새로운 Instruments의 일부 기능을 간단히 살펴봤으니 앱을 프로파일링하고 코드를 최적화해 봅시다 먼저 Xcode에서 프로젝트를 불러와 Command+i를 누르면 앱이 컴파일되고 Instruments가 열리고 File Squizer 앱의 대상이 사전 선택되는데 이제 템플릿 선택기에서 Swift 동시성 옵션을 선택하고 코딩을 시작할 수 있죠
다시 한번 앱으로 대용량 파일을 끌어올게요
이번에도 앱 실행이 멈추고 UI가 응답하지 않는데 좀 더 실행되도록 놔둬서 Instruments가 앱에 대한 모든 정보를 캡처하도록 하겠습니다
이제 트레이스가 확보됐으니 조사를 시작할 수 있어요 모든 정보가 잘 보이도록 트레이스를 전체 화면으로 보여드릴게요
Option 키를 누르고 드래그하면 보고 싶은 부분은 확대할 수 있죠
프로세스 트랙을 보면 Instruments가 UI가 정지된 정확한 위치를 표시해줍니다 이 기능은 정지가 언제 얼마동안 발생했는지 명확하지 않은 경우에 유용할 수 있죠 앞서 말했듯이 최상위 Swift 태스크 통계부터 보는 것이 좋습니다 가장 먼저 눈에 띄는 것은 실행 중인 태스크 수인데요 대부분의 경우 하나의 작업만 실행되는데 모든 작업의 강제 직렬화가 문제의 일부임을 알 수 있죠 작업 상태 요약을 통해 가장 오래 실행 중인 작업을 찾고 해당 작업을 타임라인에 고정할 수도 있는데요
해당 트랙의 내러티브 보기를 보면 백그라운드에서 짧게 실행된 다음 메인 스레드에서 오래 실행된 걸 알 수 있습니다 메인 스레드를 타임라인에 고정하면 더 자세히 조사할 수 있어요
메인 스레드는 여러 장기 실행 작업에 의해 블로킹되었는데요 이게 바로 Mike가 말한 메인 행위자 블로킹 문제의 예입니다 여기서 스스로에게 던져야 할 질문은 이 태스크는 무엇을 수행하며 어디에서 왔는지인데요 내러티브 보기로 돌아가면 두 질문에 대한 답을 구할 수 있죠 세부 사항 보기의 생성 경로 역추적 정보를 보면 compressAllFiles 함수에서 태스크가 생성됐음을 알 수 있으며 내러티브 보기는 태스크가 compressAllFiles의 클로저 1을 실행함을 보여줍니다 이 기호를 우클릭하면 소스 뷰어에서 열 수 있는데요
이 함수의 클로저 1은 압축 파일을 호출하고 있죠 이제 이 태스크가 생성된 위치와 수행 중인 작업을 알았으므로 Xcode에서 코드를 열고 조정함으로써 메인 스레드에서 이런 복잡한 계산을 수행하는 것을 피할 수 있습니다 compressAllFiles 함수는 CompressionState 클래스 내에 있고 CompressionState 클래스는 @MainActor에서 실행되게 되어있는데 그래서 태스크도 메인 스레드에서도 실행된 거예요 @Published 속성은 메인 스레드에서만 업데이트되어야 하므로 이 클래스는 MainActor에 있어야 하며 그렇지 않으면 런타임 문제가 발생할 수 있죠 따라서 대신에 이 클래스를 자체 행위자로 변환할 수 있는데 컴파일러에는 해당 작업을 수행할 수 없다고 뜹니다 기본적으로 가변 상태는 2개의 다른 행위자로 보호되어야 한다고 했기 때문이죠 그런데 여기서 해결책의 실마리를 찾을 수 있습니다 이 클래스에는 2개의 서로 다른 변경 가능한 상태가 있는데 상태 중 하나인 'files' 속성은 SwiftUI에 의해 관찰되므로 메인 행위자로 분리해야 하죠 한편 다른 상태인 'logs'에 대한 액세스는 동시 접근으로부터 보호되어야 하지만 특정 시점에 어떤 스레드가 로그에 접근하는지는 중요치 않으므로 실제로 메인 행위자에 있을 필요는 없는데 그래도 동시 접근으로부터 보호해야 하므로 자체 행위자 내에 두도록 합니다 이제 필요 시 태스크가 둘 사이를 이동할 방법을 추가하면 되는데 우선 ParallelCompressor라는 새로운 행위자를 만든 다음
logs 상태를 새 행위자에 복사하고 설정 코드를 추가하죠
이제 행위자들간에 정보 전달이 이루어지도록 해야 하는데 먼저 CompressionState 클래스에서 logs 변수를 참조하는 코드를 제거하고 ParallelCompressor 행위자에 추가할게요
그런 다음 마지막으로 CompressionState를 업데이트하여 ParallelCompressor에서 compressFile을 호출하도록 합니다
변경된 코드로 앱을 다시 실행해보죠 이번에도 앱에 대용량 파일을 끌어오겠습니다
이제 UI가 정지되지는 않으니 전보다 훨씬 나아졌지만 아직 속도가 기대에 미치지 못하네요 작업 시간을 최소화하려면 머신의 모든 코어를 최대한 활용해야 하는데 Mike, 그 밖에 봐야 할 부분이 있나요?
메인 행위자로부터 작업을 이동시켜 문제는 해결했지만 여전히 원하는 성능을 얻지 못하고 있는데요 그 이유를 알려면 행위자를 자세히 살펴봐야 합니다 행위자는 여러 작업의 공유 상태를 안전하게 조작하게 해주지만 공유 상태에 대한 액세스를 직렬화하는 방식이기 때문에 한 번에 하나의 작업만 행위자를 점유할 수 있으며 그 동안 해당 행위자를 사용해야 하는 다른 작업은 대기해야 하죠 Swift 동시성은 비정형 작업과 작업 그룹, Async let을 사용하여 병렬 계산을 할 수 있게 해주며 이러한 구성은 많은 CPU 코어를 동시에 사용할 수 있습니다 이런 코드의 행위자를 사용할 경우 작업 간에 공유되는 행위자에서 많은 작업을 수행하지 않도록 주의해야 하죠 여러 작업이 동일한 행위자를 동시에 사용려고 하면 행위자가 해당 작업의 실행을 직렬화되므로 병렬 컴퓨팅이 주는 성능상의 장점을 누릴 수 없습니다
행위자를 사용할 수 있을 때까지 각 작업이 기다려야 하기 때문이죠 이를 해결하려면 행위자 데이터에 대한 독점 접근 권한이 꼭 필요한 경우에만 행위자에서 작업을 실행하고 그 외엔 모두 행위자 밖에서 실행되어야 합니다 작업을 청크로 나누어 일부 청크만 행위자에서 실행하도록 하는 거죠 행위자 외 실행으로 분류된 청크는 병렬로 실행할 수 있는데요 컴퓨터가 작업을 훨씬 빨리 끝낼 수 있다는 뜻입니다 실제 사례를 살펴보죠 고마워요, Mike 업데이트된 'File Squeezer' 앱의 트레이스를 보면서 Mike가 방금 말한 내용을 생각해보죠 태스크 요약을 보면 동시성 코드가 큐에 작업을 넣은 상태에서 엄청난 시간을 소요하고 있는데 행위자에 대한 독점적 액세스를 기다리는 작업이 많다는 뜻입니다 작업 하나를 고정하여 그 이유를 알아보죠
이 작업은 압축 실행 전 ParallelCompressor 행위자에 연결되기를 기다리는 과정에 상당한 시간을 소비합니다 행위자를 타임라인에 고정해보죠
보시면 ParallelCompressor 행위자에 대한 최상위 데이터가 있는데요 이 행위자 대기열은 장기 실행 작업에 의해 블로킹된 듯한데 태스크는 실제 필요한 시간 동안만 행위자에 남아 있어야 하죠 작업 내러티브로 돌아가 볼게요
ParallelCompressor의 대기열에 입력된 후 작업은 compressAllFiles에 있는 클로저 1에서 실행되니 거기서 조사를 시작해보죠 소스 코드를 보면 이 클로저가 압축 작업을 실행함을 알 수 있는데 compressFile 함수는 ParallelCompressor 행위자의 일부이므로 함수의 모든 작업이 행위자에서 실행되면서 다른 압축 작업을 모두 차단하게 됩니다 이를 해결하려면 행위자 격리 compressFile 함수를 분리된 작업으로 가져와야 하며
이를 통해 관련 가변 상태를 업데이트하는 데 필요한 기간 동안 행위자에서만 분리된 작업을 수행할 수 있죠 이제 compress 함수는 행위자 보호 상태에 액세스해야 할 때까지 스레드 풀의 모든 스레드에서 자유롭게 실행될 수 있습니다 예를 들어 files 속성에 액세스해야 하는 경우 메인 행위자로 이동하지만 작업이 완료되는 즉시 '동시성의 바다'로 다시 이동하죠 logs 속성에 접근해야 해서 ParallelCompressor 행위자로 갈 때까진 말이죠 다만 거기서도 작업이 끝나는 즉시 행위자를 떠나 스레드 풀에서 실행됩니다 물론 압축 작업을 해야 하는 태스크는 하나만이 아니고 여러 개일 텐데요 행위자에 제약받지 않음으로 모든 작업을 동시에 실행할 수 있으며 오직 스레드 수에 의해서만 제한됩니다
각 행위자는 한 번에 하나의 태스크만 실행할 수 있지만 대부분의 경우 태스크가 행위자에 있을 필요가 없으므로 Mike가 설명한 대로 압축 작업을 병렬로 실행하고 모든 가용 CPU 코어를 활용할 수 있죠 그럼 이제 그렇게 바꿔볼게요
compressFile 함수를 '비 독립형'이라고 표시하면
몇 가지 컴파일러 오류가 발생합니다 비 독립형이라고 표시하는 것은 이 행위자의 공유 상태에 접근할 필요가 없다고 Swift 컴파일러에 알려주는 것인데 그건 사실이 아니기 때문이죠 이 log 함수는 행위자 격리형이며 공유 가변 상태에 접근해야 합니다 문제를 해결하려면 해당 함수를 비동기식으로 만든 다음 모든 log 호출을 await 키워드로 표시해야 하죠
이제 작업 생성을 업데이트하여 분리된 작업을 만들어야 하는데
태스크가 자신이 생성된 Actorcontext를 상속하지 않도록 하려는 것인데 분리된 작업의 경우 명시적으로 self를 캡처해야 합니다
앱을 다시 테스트해보죠
이제 앱은 모든 파일을 동시에 압축할 수 있으며 UI도 응답성을 유지합니다 개선 사항을 확인려면 Swift 행위자 Instruments를 보면 되죠 ParallelCompressor 행위자를 보면 대부분의 작업은 행위자에서 단시간 동안 실행되었으며 대기열 크기는 일정 수준을 넘지 않았어요 요약하자면 Instrument를 사용하여 UI 정지의 원인을 분리하고 동시성 코드를 재구성하여 병렬 처리 능력을 향상시켰으며 데이터를 사용하여 성능 개선을 확인해봤습니다 이제 Mike가 그 밖의 잠재적인 성능 문제에 대해 말씀드리겠습니다 데모에서 살펴본 것 외에 흔히 발생하는 2가지 문제를 다루고자 하는데요 먼저 스레드 풀 고갈을 살펴보겠습니다 스레드 풀이 고갈되면 성능이 저하되거나 앱이 교착 상태에 빠질 수 있죠 Swift 동시성 코드는 작업이 실행 중일 때 앞으로 진행되도록 하며 무언가를 기다릴 때는 보통 일시 중단함으로써 대기하는데 작업 내의 코드가 일시 중단 없이 파일 또는 네트워크 IO 블로킹이나 Lock 획득과 같은 블로킹 호출을 수행할 수도 있으며 이는 작업이 앞으로 진행되어야 한다는 요건에 위배됩니다 이 경우 작업은 실행 중인 스레드를 계속 점유하지만 실제 CPU 코어는 사용하지 않는 상태인데 스레드 풀은 한정적이고 일부는 차단되므로 동시성 런타임이 모든 CPU 코어를 온전히 사용할 수 없게 되고 이는 결국 수행 가능한 병렬 계산의 양과 앱의 최대 성능 감소로 이어집니다 극단적인 경우 전체 스레드 풀이 블로킹 형식 작업에 의해 점유되고 스레드 풀에서 실행되어야 하는 새 태스크가 필요한 무언가를 기다리는 경우 동시성 런타임이 교착 상태에 빠질 수 있죠 따라서 태스크에서 블로킹 호출을 하지 않아야 하며 파일 및 네트워크 IO는 비동기 API를 사용하여 수행해야 합니다 또한 조건 변수나 세마포어에 대기하지 않도록 하세요 작은 단위의 짧게 유지되는 Lock은 허용되지만 경합이 많거나 장기간 유지되는 Lock은 피해야 합니다 이러한 작업을 수행해야 하는 코드가 있는 경우 해당 코드를 분리된 대기열에서 실행하는 등의 방법으로 동시성 스레드 풀 외부로 이동시키고 연속성을 사용하여 동시성의 세계에 연결하도록 하고 가능하면 항상 비동기 API로 작업을 블로킹함으로써 시스템이 원활하게 작동하도록 하세요 연속성을 사용하는 경우 제대로 사용하도록 주의해야 합니다 연속성은 Swift 동시성과 다른 형태의 비동기 코드를 잇는 다리로 현재 작업을 일시 중단하고 호출 시 재개하는 콜백 기능을 제공하는데 이는 이후에 콜백 기반 비동기 API와 함께 사용될 수 있죠 Swift 동시성의 관점에서는 작업이 일시 중단된 다음 연속성이 재개될 때 다시 시작되는 한편 콜백 기반 비동기 API 관점에서는 작업이 시작되고 완료될 때 콜백이 호출됩니다 Swift 동시성 Instruments는 연속성에 대해 알고 그에 따라 시간 간격을 표시하여 작업이 연속성 호출을 기다리고 있음을 보여주죠 연속성 콜백에는 특별한 요건이 있는데 더도 말고 덜도 말고 정확히 한 번만 호출해야 한다는 겁니다 이는 콜백 기반 API의 일반적인 요구 사항이지만 비공식적인 경향이 있고 Swift 언어에서 강제되지 않기 때문에 흔히 간과되고는 하죠 Swift 동시성 또한 이러한 요건 준수를 어렵게 합니다 콜백이 2번 호출되면 프로그램이 충돌하거나 오작동하고 호출되지 않으면 태스크가 누수되기 때문입니다 이 코드 스니펫에서는 withCheckedContinuation을 사용하여 연속성을 얻은 다음 콜백 기반 API를 호출하죠 콜백에서는 연속성을 재개하는데 이는 정확히 한 번만 호출해야 한다는 요건을 충족합니다 코드가 더 복잡할 경우 주의를 기울여야 합니다 왼쪽 코드는 성공일 경우에만 연속성을 재개하도록 콜백을 수정했는데 이건 버그입니다 실패할 경우 연속성이 재개되지 않고 작업이 영원히 중단되기 때문이죠 오른쪽 코드는 연속성을 2번 재개하도록 하는데 이것 또한 버그로 앱이 오작동하거나 충돌하게 됩니다 두 스니펫 모두 연속성을 정확히 1번 재개해야 한다는 요건에 위배되죠 연속성 코드는 Checked와 Unsafe 2가지 유형이 있는데 성능이 절대적으로 중요한 경우가 아니면 항상 연속 작업에는 withCheckedContinuation API를 사용하세요 Checked 연속성 코드는 오용을 자동 감지하여 오류 경고를 띄우고 Checked 연속성이 2번 호출되면 연속성을 트랩하며 연속성이 아예 호출되지 않을 경우 연속성 손상 시 콘솔에 메시지가 출력되어 연속성 누출을 알려주죠 Swift 동시성 Instruments는 연속 상태에서 무기한 중단된 해당 작업을 표시합니다 Instruments의 새 Swift 동시성 템플릿에는 이 밖에도 많은 기능이 있어요 구조화된 동시성을 그래픽으로 시각화하거나 작업 생성 호출 트리 뷰를 얻을 수 있으며 정확한 어셈블리 지침을 탐구하면 Swift 동시성 런타임의 전체적인 그림을 볼 수도 있죠 Swift 동시성의 작동 원리에 대한 자세한 내용은 작년 세션의 'Swift 동시성의 이면' 영상을 참조하시고 데이터 경합에 대한 자세한 사항은 'Swift 동시성을 활용한 데이터 경합 제거' 영상을 참고하세요 시청해 주셔서 감사합니다! 동시성 코드 디버깅을 즐겨보세요
-
-
10:24 - CompressionState class
@MainActor class CompressionState: ObservableObject { @Published var files: [FileStatus] = [] var logs: [String] = [] func update(url: URL, progress: Double) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].progress = progress } } func update(url: URL, uncompressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].uncompressedSize = uncompressedSize } } func update(url: URL, compressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].compressedSize = compressedSize } } func compressAllFiles() { for file in files { Task { let compressedData = compressFile(url: file.url) await save(compressedData, to: file.url) } } } func compressFile(url: URL) -> Data { log(update: "Starting for \(url)") let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in update(url: url, uncompressedSize: uncompressedSize) } progressNotification: { progress in update(url: url, progress: progress) log(update: "Progress for \(url): \(progress)") } finalNotificaton: { compressedSize in update(url: url, compressedSize: compressedSize) } log(update: "Ending for \(url)") return compressedData } func log(update: String) { logs.append(update) }
-
11:49 - CompressionState class using ParallelCompressor actor
actor ParallelCompressor { var logs: [String] = [] unowned let status: CompressionState init(status: CompressionState) { self.status = status } func compressFile(url: URL) -> Data { log(update: "Starting for \(url)") let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in Task { @MainActor in status.update(url: url, uncompressedSize: uncompressedSize) } } progressNotification: { progress in Task { @MainActor in status.update(url: url, progress: progress) await log(update: "Progress for \(url): \(progress)") } } finalNotificaton: { compressedSize in Task { @MainActor in status.update(url: url, compressedSize: compressedSize) } } log(update: "Ending for \(url)") return compressedData } func log(update: String) { logs.append(update) } } @MainActor class CompressionState: ObservableObject { @Published var files: [FileStatus] = [] var compressor: ParallelCompressor! init() { self.compressor = ParallelCompressor(status: self) } func update(url: URL, progress: Double) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].progress = progress } } func update(url: URL, uncompressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].uncompressedSize = uncompressedSize } } func update(url: URL, compressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].compressedSize = compressedSize } } func compressAllFiles() { for file in files { Task { let compressedData = await compressor.compressFile(url: file.url) await save(compressedData, to: file.url) } } } }
-
17:46 - CompressionState class using ParallelCompressor with minimal actor-isolation and detached tasks
actor ParallelCompressor { var logs: [String] = [] unowned let status: CompressionState init(status: CompressionState) { self.status = status } nonisolated func compressFile(url: URL) async -> Data { await log(update: "Starting for \(url)") let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in Task { @MainActor in status.update(url: url, uncompressedSize: uncompressedSize) } } progressNotification: { progress in Task { @MainActor in status.update(url: url, progress: progress) await log(update: "Progress for \(url): \(progress)") } } finalNotificaton: { compressedSize in Task { @MainActor in status.update(url: url, compressedSize: compressedSize) } } await log(update: "Ending for \(url)") return compressedData } func log(update: String) { logs.append(update) } } @MainActor class CompressionState: ObservableObject { @Published var files: [FileStatus] = [] var compressor: ParallelCompressor! init() { self.compressor = ParallelCompressor(status: self) } func update(url: URL, progress: Double) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].progress = progress } } func update(url: URL, uncompressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].uncompressedSize = uncompressedSize } } func update(url: URL, compressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].compressedSize = compressedSize } } func compressAllFiles() { for file in files { Task.detached { let compressedData = await self.compressor.compressFile(url: file.url) await save(compressedData, to: file.url) } } } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.