
-
Swift 동시성 사용하기
Swift의 주요 동시성 개념을 알아보세요. 동시성은 앱 반응성과 성능을 향상하는 데 도움이 되고 Swift는 비동기성 및 동시성 코드를 올바르게 작성하기 쉽도록 설계되었습니다. 단일 스레드에서 동시성으로 전환하는 데 필요한 단계를 살펴보세요. 또한 더욱 비동기적인 코드를 만들든, 해당 코드를 백그라운드로 이동시키든, 동시 작업 전반에서 데이터를 공유하든 Swift 동시성 기능을 최대한 활용하는 방법과 시기를 결정하는 데 도움을 드립니다.
챕터
- 0:00 - 서론
- 3:17 - 단일 스레드 코드
- 6:00 - 비동기식 작업
- 7:24 - 교차 실행
- 10:22 - 동시성 소개
- 11:07 - 동시 함수
- 13:10 - 비격리 코드
- 14:13 - 동시 스레드 풀
- 14:58 - 데이터 공유
- 15:49 - 값 유형
- 17:16 - 액터 격리 유형
- 18:30 - 클래스
- 23:18 - 액터
- 26:12 - 요약
리소스
관련 비디오
WWDC25
WWDC23
-
비디오 검색…
안녕하세요! Swift 팀의 Doug입니다 앱에서 Swift 동시성을 최대한 활용하는 법을 알려드리게 되어 기쁩니다 동시성을 통해 코드는 동시에 여러 작업을 수행합니다 동시성을 사용하면 디스크의 파일을 읽거나 네트워크 요청을 수행하는 등 데이터가 오기를 기다리는 상황에서 반응성이 향상됩니다 대용량 이미지 처리 등 비용이 많이 드는 연산을 백그라운드로 오프로드할 수도 있죠 Swift의 동시성 모델은 동시성 코드를 작성하기 쉽도록 설계되었습니다 동시성의 도입을 명확히 보여 주면서 동시 작업 간에 공유되는 데이터를 식별합니다 이 정보를 활용해 컴파일 시 발생 가능한 데이터 경합을 식별할 수 있어 필요에 따라 동시성을 도입할 수 있습니다 데이터 경합이 발생할까 봐 두려워하지 않아도 되죠
많은 앱에서 동시성을 조금만 사용해야 하고 동시성이 전혀 필요하지 않은 경우도 있습니다 동시성 코드는 단일 스레드 코드보다 복잡하고 필요할 때만 도입해야 합니다
앱은 모든 코드를 메인 스레드에서 실행하며 시작하고 단일 스레드 코드만으로도 많은 것이 가능합니다 앱은 메인 스레드에서 UI 관련 이벤트를 수신하고 대응하여 UI를 업데이트합니다 앱에서 연산을 많이 수행하지 않으면 모두 메인 스레드에 배치해도 괜찮습니다 결국 네트워크를 통해 콘텐츠를 가져오는 등의 비동기 코드를 도입할 가능성이 높아집니다 코드는 콘텐츠가 네트워크를 거쳐 올 때까지 대기하며 UI가 멈추지 않도록 합니다 작업 실행이 너무 오래 걸리면 메인 스레드와 동시 실행되는 백그라운드 스레드로 옮길 수 있습니다
앱을 계속 개선해 나가면서 모든 데이터를 메인 스레드 내에 유지하면서 앱의 성능이 저하되고 있음을 알 수 있습니다 이제 특정 용도로 항상 백그라운드에서 실행되는 데이터 유형을 소개합니다
Swift 동시성은 이런 종류의 동시 작업을 표현하기 위해 액터와 작업 같은 도구를 제공합니다 대규모 앱은 비슷한 아키텍처일 가능성이 높지만 꼭 그런 것은 아니며 모든 앱이 그렇게 될 필요는 없죠 이 세션에서는 단일 스레드에서 동시성까지 앱이 발전해 가는 과정에서 거쳐야 하는 단계를 차례로 설명하겠습니다 과의 각 단계마다 언제 그 단계를 수행하고 Swift 언어의 어떤 기능을 사용하며, 효과적인 사용 방법 및 그렇게 작동하는 이유를 이해하도록 돕겠습니다 단일 스레드 코드가 Swift 동시성과 연동되는 방식을 설명하고 네트워크 접속과 같은 지연이 많이 되는 작업에 유용한 비동기 작업을 소개합니다 동시성을 도입해 작업을 백그라운드 스레드로 이동하고 데이터 경합 없는 스레드 간 데이터 공유 방법을 알아본 후 액터를 사용해 데이터를 메인 스레드에서 옮깁니다 단일 스레드 코드로 시작해 보겠습니다 앱을 실행하면 코드가 메인 스레드에서 실행됩니다 코드를 추가하면 명시적으로 동시성을 도입해 실행 위치를 바꾸기 전까지 메인 스레드에서 계속 실행되죠 단일 스레드 코드는 작성 및 유지 관리하기가 쉬운데 코드가 한 번에 한 작업만 수행하기 때문입니다 나중에 동시성을 도입하면 Swift에서 메인 스레드 코드를 보호합니다
메인 스레드와 그 데이터는 메인 액터로 표현되고 메인 액터에는 동시성이 적용되지 않는데 메인 스레드에서만 이를 실행할 수 있기 때문입니다 @MainActor 표기를 사용해 메인 액터에 속함을 지정합니다 Swift는 메인 액터 코드가 메인 스레드에서만 실행되고 거기서만 그 데이터에 액세스하도록 보장합니다 코드는 메인 액터에 격리됩니다 Swift는 메인 액터를 사용해 메인 스레드 코드를 보호합니다 Swift 컴파일러가 모듈의 모든 항목에 자동으로 @MainActor를 쓰는 것과 같습니다 코드의 아무 위치에서나 정적 변수와 같은 공유 상태에 접근할 수 있습니다 메인 액터 모드인 경우 동시성을 도입하기까지는 동시 접근에 대해 걱정하지 않아도 됩니다 메인 액터를 사용한 코드 보호는 빌드 설정으로 제어되며 주로 메인 앱 모듈과 UI 상호작용 중심의 모듈에 사용하는 것이 좋습니다 Xcode 26을 사용한 새로운 앱 프로젝트에는 기본적으로 이 모드가 활성화됩니다 이 세션의 코드 예제 전체에서 메인 액터 모드가 활성화된 것으로 가정하겠습니다
URL에서 가져온 이미지를 표시하는 모델에 메서드를 추가해 봅시다 로컬 파일의 이미지를 로드한 다음 디코딩하고 UI에 이미지를 표시하려 합니다 앱에는 동시성이 전혀 적용되지 않았습니다 모든 작업을 수행하는 메인 스레드 하나뿐입니다 전체 기능이 하나로 메인 스레드에서 실행됩니다 모든 작업이 충분히 빠르기만 하면 괜찮아요
지금은 로컬에서 파일을 읽는 것만 가능합니다 앱이 네트워크를 통해 이미지를 가져오도록 하려면 다른 API를 사용해야 합니다
URL이 주어지면 URLSession API로 네트워크를 통해 데이터를 가져옵니다 메인 스레드에서 이 메서드를 실행하면 데이터 다운로드를 마칠 때까지 UI가 정지됩니다 개발자에게는 앱의 반응성 유지가 중요합니다 즉, UI가 끊기거나 멈출 정도로 오랫동안 메인 스레드를 점유하지 않도록 주의해야 합니다 Swift 동시성은 네트워크 요청과 같은 네트워크 요청 등 데이터가 오기를 기다릴 때 메인 스레드를 점유하지 않고 비동기 작업을 사용합니다 멈춤을 방지하려면 네트워크 접근이 비동기라야 하죠 비동기 호출을 처리하도록 fetchAndDisplayImage를 변경하려면 함수를 ‘async’로 정의하고 ‘await’로 URL session API를 호출합니다 await는 함수가 중단되는 경우를 나타내며 현재 스레드에서 대기 중인 이벤트가 발생할 때까지 실행이 중지된 다음 실행이 재개됩니다
함수를 두 개로 나누는 것과 같은데 이미지 가져오기가 시작되기 전까지 실행되는 부분과 이미지를 가져온 다음 실행되는 부분이죠 함수를 이렇게 분리하면 두 코드 부분 사이에 다른 작업을 실행할 수 있게 되어 UI가 계속 반응성 있게 유지됩니다
URLSession과 같은 라이브러리 API는 자동으로 작업을 백그라운드로 오프로드합니다 아직 코드에 동시성을 도입하지 않은 건 필요하지 않았기 때문이죠! 앱의 일부분을 비동기로 정의하고 자동으로 작업을 오프로드하는 API를 호출해 반응성을 개선했습니다 코드에서 async/await를 정의하는 것이 다였죠
지금까지 코드는 비동기 함수 하나만 실행하고 있습니다 비동기 함수는 작업에서 실행되고 작업은 다른 코드와는 독립적으로 실행되며 특정 연산을 처음부터 끝까지 수행하도록 만들어집니다 버튼 누르기 등 이벤트에 답하여 작업을 생성합니다 이 경우 작업은 전체 fetch-and-display image 연산을 수행합니다 주어진 앱에는 비동기 작업이 여러 개일 수 있습니다 fetch-and-display image 작업 외에 뉴스를 가져와서 표시한 다음 새로 고칠 때까지 대기하는 작업을 또 하나 추가했습니다 각 작업은 처음부터 끝까지 순서대로 연산을 완료합니다 가져오기는 백그라운드에서 실행되지만 다른 연산은 모두 메인 스레드에서 한 번에 하나의 연산만 실행됩니다 각 작업은 서로 간에 독립적이므로 메인 스레드에서 교대로 실행됩니다 메인 스레드는 실행 준비가 되는 대로 각 작업의 일부를 실행합니다 단일 스레드가 여러 작업을 번갈아가며 ‘교차 실행’하면 리소스를 가장 효율적으로 사용함으로써 성능이 향상됩니다 스레드는 단일 연산을 대기하는 동안 스레드를 유휴 상태로 두기 보다는 가능한 한 빨리 아무 작업이든 진행합니다 뉴스 가져오기 전에 이미지 가져오기가 완료되면 메인 스레드는 뉴스를 표시하기 전에 이미지를 디코딩하여 표시합니다 뉴스 가져오기가 먼저 끝난다면 이미지를 디코딩하기 전에 뉴스를 먼저 표시하겠죠
앱이 여러 개의 독립적인 연산을 동시에 수행해야 할 때는 여러 개의 비동기 작업이 유용합니다 작업을 특정 순서로 수행해야 하는 경우 해당 작업은 단일 작업으로 실행해야 합니다
네트워크 요청과 같이 대기 시간이 긴 연산이 있을 때 앱의 반응성을 유지하려면 비동기 작업을 사용해 지연이 드러나지 않게 합니다 라이브러리는 코드가 계속 메인 스레드에서 실행되는 동안 자동으로 약간의 동시성을 수행하는 비동기 API를 제공하므로 도움이 됩니다 URLSession API는 자동으로 약간의 동시성을 도입했는데 네트워크 접근을 백그라운드 스레드에서 처리하기 때문이죠 fetch-and-display image 연산은 메인 스레드에서 실행되는데 디코딩 연산이 너무 오래 걸려 대용량 이미지를 디코딩할 때 UI가 멈추는 식으로 나타날 수 있습니다
비동기, 단일 스레드만으로 앱을 운영하기에 충분할 수 있죠 하지만 앱이 반응하지 않음을 깨닫기 시작하면 메인 스레드에서 너무 많은 작업이 실행되고 있다는 신호죠 Instruments와 같은 프로파일링 도구를 사용해 어디에 시간을 많이 쓰는지 확인해 볼 수 있습니다 동시성 없이 처리할 수 있는 작업을 먼저 처리하고 빨리 처리할 수 없다면 동시성을 도입해야 할 수 있습니다 동시성을 통해 코드 중 일부가 백그라운드에서 메인 스레드와 병렬 실행되므로 UI가 중단되지 않고 시스템의 CPU 코어를 더 많이 사용해 작업도 더 빨리 완료됩니다 목표는 디코딩을 메인 스레드에서 옮겨 백그라운드 스레드에서 실행되도록 하는 것입니다 메인 액터 모드가 기본 설정되어 있으므로 fetchAndDisplaylmage와 decodelmage는 모두 메인 액터로 격리되어 있습니다 메인 액터 코드는 메인 스레드에서만 접근할 수 있는 모든 데이터와 코드에 접근할 수 있으며 동시성이 적용되지 않아서 안전합니다
decodeImage 호출을 오프로드하고 싶으면 @concurrent 속성을 decodeImage 함수에 적용하면 됩니다 @concurrent는 Swift에 백그라운드 실행을 지시합니다 decodeImage 실행 위치를 변경하면 decodeImage로 접근 가능 상태에 대한 가정도 변경돼요 어떻게 구현되었는지 볼까요 구현은 메인 액터에 저장된 캐시 이미지 데이터의 사전을 확인하는 것으로 메인 스레드에서만 실행됩니다 Swift 컴파일러는 함수의 어느 부분이 메인 액터의 데이터에 접근하는지 보여 줍니다 동시성을 추가할 때 버그가 발생하지 않도록 하기 위해 꼭 확인해야 하는 부분입니다 메인 액터와의 연결을 분리할 때 전략을 사용하여 안전하게 동시성을 도입할 수 있습니다 메인 액터 코드를 항상 메인 액터에서 실행되는 호출자로 이동하는 방법을 사용할 수도 있습니다 작업이 동기적으로 실행되도록 할 때 유용한 전략입니다 await로 동시성 코드에서 비동기적으로 메인 액터에 접근하되
코드가 메인 액터에 소속되지 않아도 된다면 nonisolated 키워드를 추가해 액터에서 분리합니다 먼저 첫 번째 전략을 살펴보고 나머지는 나중에 이야기하겠습니다 이미지 캐싱을 메인 액터에서 실행되는 fetchAndDisplayImage로 이동합니다 호출 전에 캐시를 확인해 지연을 줄입니다 이미지가 캐시에 있으면 fetchAndDisplayImage는 전혀 중단되지 않고 동기적으로 완료됩니다 즉, 결과가 UI로 즉시 전달되고 이미지가 준비되지 않은 경우에만 일시 중단됩니다
decodeImage에서 더 이상 필요하지 않은 url 매개변수를 제거할 수 있습니다 이제 decodeImage의 결과를 기다리기만 하면 됩니다
@concurrent 함수는 항상 액터에서 벗어나서 실행됩니다 함수를 호출하는 액터에서 계속 실행되도록 하려면 nonisolated 키워드를 사용하면 됩니다 Swift로 더 많은 동시성을 적용하는 방법이 또 있습니다 자세한 건 ‘구조화된 동시성의 기초를 넘어’를 참고하세요
클라이언트에서 사용할 라이브러리로 디코딩 API 제공 시 @concurrent 사용만이 API를 잘 선택한 것은 아닙니다 데이터를 디코딩하는 시간은 데이터 크기에 따라 다르고 소량 데이터 디코딩은 메인 스레드에서 해도 괜찮습니다 라이브러리의 경우 nonisolated API를 제공하고 클라이언트에서 오프로드 여부를 결정하게 하세요
Nonisolated 코드는 어디서나 호출할 수 있습니다 메인 액터에서 호출하면 메인 액터에서 실행되고 백그라운드 스레드에서 호출하면 거기서 실행됩니다 범용 라이브러리의 기본값이 된 이유죠 작업을 오프로드하면 시스템에서 백그라운드 실행 예약을 처리합니다 동시 스레드 풀에는 시스템의 백그라운드 스레드가 포함되고 관련 스레드의 수는 제한되지 않습니다 시계와 같은 소형 기기의 경우 풀 안에 스레드가 하나 또는 두 개만 있을 수 있습니다 대규모 시스템은 백그라운드 스레드가 더 많습니다 어떤 백그라운드 스레드에서 실행되는지는 중요하지 않으며 시스템에서 자동으로 리소스를 최대한 활용합니다 작업이 일시 중단되는 경우 원래 스레드에서 준비된 다른 작업이 실행됩니다 작업이 재개되면 동시성 풀의 사용 가능한 스레드에서 실행되며 원래 백그라운드 스레드에서 실행되지 않을 수 있습니다
동시성을 통해 스레드 간에 데이터를 공유합니다 동시성 코드의 가변 상태 공유 시 해결하기 어려운 런타임 버그가 발생하기 쉽습니다 Swift로 컴파일 시 오류를 식별해 낼 수 있으므로 자신감을 가지고 동시성 코드를 작성하면 됩니다 메인 액터와 동시성 풀 간에 이동할 때마다 서로 다른 스레드 간에 데이터를 공유합니다 UI에서 URL을 가져오면 이미지 가져오기가 메인 액터에서 벗어나 백그라운드 스레드로 전달됩니다 이미지 데이터가 반환되면 이미지 디코딩으로 전달됩니다 이미지 디코딩 후 이미지는 self와 함께 메인 액터로 다시 전달됩니다 Swift는 동시성 코드의 이 값에 접근하도록 보장합니다 UI 업데이트로 인해 URL 포함 추가 작업이 생성되면 어떻게 될까요 URL은 값 유형이므로 URL을 백그라운드 스레드에 복사하는 경우 메인 스레드와 별도로 백그라운드 스레드에 사본이 있습니다 사용자가 UI를 통해 새 URL을 입력하면 메인 스레드의 코드는 사본을 사용하거나 수정해도 백그라운드 스레드 사용 값에 변경이 적용되지 않습니다 즉 URL과 같은 값 유형을 안전하게 공유할 수 있습니다 실제로 공유하지 않고 각각의 사본은 다른 사본과 별도이기 때문입니다
값 유형은 Swift가 처음 등장할 때부터 중요한 부분이었고 문자열, 정수, 날짜 등 모든 기본 유형은 값 유형입니다
사전 및 배열과 같은 값 유형 모음 또한 값 유형입니다 값 유형을 저장하는 구조체와 열거형도 마찬가지입니다 이 Post 구조체처럼요 동시적으로 공유할 수 있는 유형은 Sendable 유형입니다 Sendable 프로토콜을 준수하는 유형은 안전하게 공유할 수 있죠 Array와 같은 모음은 Sendable에 대한 조건부 적합성을 정의하므로 요소가 있을 때만 Sendable입니다 구조체와 열거형은 모든 인스턴스 데이터가 Sendable일 때 Sendable로 표시됩니다 메인 액터 유형은 암묵적으로 Sendable이므로 명시적으로 정의하지 않아도 됩니다 메인 액터와 같은 액터는 한 번에 한 작업만 상태에 접근하게 하여 Sendable이 아닌 상태를 보호합니다 액터는 메서드로 전달된 값을 저장하고 보호 중인 상태에 대한 참조를 메서드를 통해 반환할 수도 있죠 액터로, 혹은 액터로부터 값이 전송될 때 Swift 컴파일러는 동시성 코드로 전송하기에 안전한지 확인합니다 decodeImage에 대한 비동기 호출을 집중적으로 살펴보죠
Decode image는 인스턴스 메서드로 암묵적인 self 인수를 전달합니다
두 값이 메인 액터 외부로 전송되고 결과 값이 메인 액터로 다시 전송되는 모습을 볼 수 있죠 ‘self’는 이미지 모델 클래스로 메인 액터로 격리됩니다 메인 액터는 가변 상태를 보호하므로 클래스에 대한 참조를 백그라운드 스레드로 안전하게 전달할 수 있습니다 Data는 값 유형이므로 Sendable입니다
이미지 유형만 남았네요 Data와 같은 값 유형인 경우 Sendable입니다 클래스와 같은 Sendable이 아닌 유형에 대해 이야기해 보죠 클래스는 참조 유형이므로 변수를 다른 변수에 할당할 경우 두 변수 모두 메모리 내의 동일한 객체를 가리키게 됩니다 한 변수를 통해 이미지 크기를 조정하는 등 객체를 변경할 경우 동일한 객체를 가리키는 다른 변수를 통해 변경사항을 즉시 확인할 수 있습니다 fetchAndDisplayImage는 값을 동시적으로 사용하지 않고 decodeImage는 백그라운드에서 실행되므로 메인 액터로 보호되는 상태에 접근하지 못하고 주어진 데이터로부터 이미지의 새 인스턴스를 생성하죠 이 이미지는 동시성 코드로도 참조할 수 없기 때문에 메인 액터로 안전하게 전송된 후 UI에서 표시됩니다 동시성을 도입하면 어떻게 되는지 살펴보겠습니다 scaleAndDisplay 메서드는 메인 스레드에 이미지를 로드합니다 이미지 변수는 사진이 포함된 이미지 객체를 가리킵니다 함수는 동시성 풀에서 실행되는 작업을 생성하고 이미지의 사본을 얻습니다 메인 스레드는 이미지 표시로 넘어갑니다 문제가 생겼네요 백그라운드 스레드에서 너비와 높이를 다르게 이미지를 변경하고 조정 버전 픽셀로 픽셀을 교체하는 작업을 수행합니다 이와 동시에 메인 스레드에서는 기존 너비와 높이 기준 픽셀을 순회하며 반복합니다 데이터 경합 상황이죠 프로그램이 픽셀 배열의 경계를 벗어나서 접근을 시도하면 UI 오류가 발생하거나 충돌이 발생할 가능성이 커집니다 Swift 동시성은 코드가 Sendable이 아닌 유형을 공유하려 하면 동시 작업이 이미지를 캡처하는 동시에 메인 액터에서 이미지를 표시하는 데 사용된다는 컴파일러 오류를 표시하여 데이터 경합을 방지합니다 이를 해결하려면 동일한 객체를 동시적으로 공유하지 않아야 하죠 UI에서 이미지 효과를 표시하려면 이미지가 표시되기 전 크기 조정이 완료되기를 기다리는 것이 좋습니다 세 개의 연산을 모두 순서대로 수행되는지 확인하는 작업으로 옮길 수 있습니다 displayImage는 메인 액터에서 실행되어야 하므로 await를 사용해 동시 작업에서 호출합니다 scaleAndDisplay를 async로 직접 정의할 수 있으면 코드를 단순화하고 scaleAndDisplay를 호출하는 작업에서 3개의 연산을 순서대로 수행합니다 이미지를 메인 액터로 보내 UI에 표시되면 메인 액터에서 이미지 객체를 캐싱하는 등 이미지에 대한 참조를 저장할 수 있습니다 UI에 표시된 이미지를 바꾸려 하면 안전하지 않은 동시 접근에 대한 컴파일러 오류가 발생합니다 메인 액터로 이미지를 넘기기 전에 이미지를 변경하면 문제가 해결됩니다 데이터 모델에서 클래스를 사용하는 경우 모델 클래스는 메인 액터에서 시작될 가능성이 높아서 일부분을 UI에 표시할 수 있습니다 백그라운드 스레드에서 모델로 작업하기로 결정한다면 nonisolated로 설정하면 됩니다 Sendable 상태로 되면 안 됩니다 모델 일부는 메인 스레드에서 업데이트되고 다른 부분은 백그라운드 스레드에서 업데이트되는 것을 원하지 않을 테니까요 모델 클래스를 Sendable이 아닌 상태로 유지하면 이런 종류의 수정이 발생하지 않습니다 쉬워집니다 클래스를 Sendable로 정의하면 낮은 수준의 동기화 메커니즘을 사용하기 때문입니다 클래스 처럼 클로저도 공유된 상태를 생성합니다 크기를 조정하여 표시하는 앞의 함수와 비슷하게 이미지 객체를 생성한 다음 perform(afterDelay:) 을 호출하면 이미지 객체의 크기를 조절할 수 있는 클로저가 제공되죠 클로저에는 동일한 이미지에 대한 또 다른 참조가 있으며 이를 이미지 변수의 캡처라고 합니다 Sendable이 아닌 클래스와 마찬가지로 클로저는 동기적으로 호출하지만 않으면 안전합니다 동기적으로 공유할 경우에만 함수를 Sendable로 정의하세요
액터와 작업 간에 데이터 전달 시 Sendable 검사가 수행되요 앱에 버그를 일으키는 데이터 경합이 일어나지 않도록 보장하기 위함이죠 일반적인 유형은 Sendable인 경우가 많으며 동시 작업 간에 자유롭게 공유됩니다 클래스와 클로저는 동시적으로 공유하기에 안전하지 않은 가변 상태를 포함할 수 있으므로 한 번에 한 작업에서 사용하는 것이 좋습니다
여전히 작업 간에 객체를 보낼 수 있지만 반드시 보내기 전에 객체를 모두 수정하기 바랍니다 비동기 작업을 백그라운드 스레드로 이동하면 메인 스레드 공간을 확보하여 앱의 반응성이 유지됩니다 메인 액터에 데이터가 많이 있으면 비동기 작업의 메인 스레드 ‘체크인’이 너무 빈번해집니다 액터를 추가로 도입하고 싶어질 수 있죠
시간이 지나며 앱이 확장되면 메인 액터 내 상태의 양도 증가할 수 있습니다 네트워크 접근 관리 등을 처리하는 하위 시스템을 소개하죠 덕분에 많은 상태가 메인 액터에 소속되게 됩니다 네트워크 관리자에서 열린 연결 세트를 처리할 때와 같이 네트워크를 통해 데이터를 가져올 때마다 액세스합니다 이런 추가 하위 시스템을 사용하기 시작하면 이전의 fetch-and-display image 작업은 더 복잡해졌습니다 백그라운드 스레드에서 실행하려 하지만 메인 스레드에 네트워크 관리자의 데이터가 있어서 메인 스레드로 넘어가야 합니다 이는 여러 작업이 메인 액터에서 동시에 코드를 실행하려 하는 경합이 발생합니다 개별 연산 속도는 빨라도 이를 수행하는 작업이 많으면 결국 UI 오류가 발생할 수 있습니다 이전에는 코드를 메인 스레드에서 옮겼죠 @concurrent 함수에 삽입하는 식으로요 모든 작업은 네트워크 관리자 데이터에 접근하는 것으로 옮기기 위해 네트워크 관리자 액터를 도입할 수 있죠 액터는 메인 액터와 마찬가지로 데이터를 격리하므로 액터에서 실행할 때만 데이터에 액세스할 수 있습니다 메인 액터와 함께 고유한 액터 유형을 정의할 수 있습니다 액터 유형은 메인 액터 클래스와 비슷합니다 메인 액터 클래스와 같이 데이터를 격리하므로 한 번에 한 스레드만 데이터에 접근할 수 있습니다 액터 유형도 Sendable이므로 액터 객체를 공유할 수 있습니다 메인 액터와 달리 액터 객체는 여러 개일 수 있고 각각 독립적입니다 액터 객체는 메인 액터처럼 단일 스레드에 묶이지 않고 메인 액터에서 액터 객체로 일부 상태를 이동하면 백그라운드 스레드에서 더 많은 코드를 실행할 수 있어 UI의 반응성을 유지하기 위해 메인 스레드를 비워 둡니다
메인 액터에 데이터를 저장할 경우 메인 스레드에서 코드가 너무 많이 실행됨을 확인하면 액터를 사용하세요 UI가 아닌 부분에 대한 데이터, 네트워크 관리 코드 등을 새 액터로 격리합니다
앱의 클래스는 액터로 사용하도록 만들어지지 않았습니다 UI 페이싱 클래스는 메인 액터에 속해야만 UI 상태와 직접 상호작용할 수 있습니다 모델 클래스는 보통 UI가 있는 메인 액터에 속하거나 Sendable이 아닌 상태로 유지되어야만 모델 동시 접근이 늘어나지 않습니다 이 세션은 단일 스레드 코드로 시작하여 요구가 커지며 지연을 드러내지 않는 비동기 작업 백그라운드 스레드에서 실행되는 동시성 코드, 데이터 접근을 메인 스레드에서 옮기는 액터를 도입했죠 여러 앱은 시간이 지남에 따라 같은 과정을 거칠 것입니다
프로파일링 도구로 언제 어떤 코드를 옮길지 식별하세요 Swift 동시성은 코드를 메인 스레드에서 분리할 수 있게 하므로 앱의 성능과 반응성이 개선됩니다
동시성 도입에 도움이 되도록 앱에 권장되는 빌드 설정이 몇 가지 있습니다 Approachable Concurrency 설정은 동시성 작업을 쉽게 만드는 향후 출시 예정 기능 모음을 활성화합니다 모든 프로젝트에서 이 설정을 채택하는 것이 좋습니다 메인 앱 모듈과 같이 주로 UI와 상호작용하는 Swift 모듈의 경우 기본 액터 격리를 ‘메인 액터’로 설정하면 달리 정의하지 않은 한, 코드가 메인 액터 소속으로 됩니다 설정을 통해 단일 스레드 앱을 작성하기가 쉬워지고 필요할 때 동시성을 도입하는 쉬운 경로를 안내합니다 Swift 동시성은 앱 개선을 지원하도록 만들어진 도구이므로 앱에 성능 문제가 있을 때 사용하여 비동기 또는 동시성 코드를 도입하는 것이 좋습니다 Swift 6 마이그레이션 가이드는 동시성에 대한 질문에 답하고 데이터 경합 시 안전을 확보하는 방법을 찾게 해줍니다 세션의 개념이 샘플 앱에서 어떻게 적용되는지 보려면 코드 따라하기 관련 세션을 시청해 보세요 감사합니다
-
-
3:20 - Single-threaded program
var greeting = "Hello, World!" func readArguments() { } func greet() { print(greeting) } readArguments() greet()
-
4:13 - Data types in a the app
struct Image { } final class ImageModel { var imageCache: [URL: Image] = [:] } final class Library { static let shared: Library = Library() }
-
4:57 - Load and display a local image
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) throws { let data = try Data(contentsOf: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() }
-
5:36 - Fetch and display an image over the network
import Foundation struct Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) throws { let (data, _) = try URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() }
-
6:10 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } } final class Library { static let shared: Library = Library() }
-
7:31 - Creating a task to perform asynchronous work
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() var url: URL = URL("https://swift.org")! func onTapEvent() { Task { do { try await fetchAndDisplayImage(url: url) } catch let error { displayError(error) } } } func displayError(_ error: any Error) { } func fetchAndDisplayImage(url: URL) async throws { } } final class Library { static let shared: Library = Library() }
-
9:15 - Ordered operations in a task
import Foundation class Image { func applyImageEffect() async { } } final class ImageModel { func displayImage(_ image: Image) { } func loadImage() async -> Image { Image() } func onButtonTap() { Task { let image = await loadImage() await image.applyImageEffect() displayImage(image) } } }
-
9:38 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data) view.displayImage(image) } func decodeImage(_ data: Data) -> Image { Image() } }
-
10:40 - Fetch and display image over the network asynchronously
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = decodeImage(data, at: url) view.displayImage(image) } func decodeImage(_ data: Data, at url: URL) -> Image { Image() } }
-
11:11 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } }
-
11:30 - Implementation of decodeImage
final class View { func displayImage(_ image: Image) { } } final class ImageModel { var cachedImage: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { if let image = cachedImage[url] { return image } // decode image let image = Image() cachedImage[url] = image return image } }
-
12:37 - Correct implementation of fetchAndDisplayImage with caching and concurrency
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var cachedImage: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> Image { // decode image Image() } }
-
13:30 - JSONDecoder API should be non isolated
// Foundation import Foundation nonisolated public class JSONDecoder { public func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T { fatalError("not implemented") } }
-
15:18 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } }
-
16:30 - Example of value types
// Value types are common in Swift import Foundation struct Post { var author: String var title: String var date: Date var categories: [String] }
-
16:56 - Sendable value types
import Foundation // Value types are Sendable extension URL: Sendable {} // Collections of Sendable elements extension Array: Sendable where Element: Sendable {} // Structs and enums with Sendable storage struct ImageRequest: Sendable { var url: URL } // Main-actor types are implicitly Sendable @MainActor class ImageModel {}
-
17:25 - Fetch over network asynchronously and decode concurrently
import Foundation class Image { } final class View { func displayImage(_ image: Image) { } } final class ImageModel { var imageCache: [URL: Image] = [:] let view = View() func fetchAndDisplayImage(url: URL) async throws { let (data, _) = try await URLSession.shared.data(from: url) let image = await self.decodeImage(data, at: url) view.displayImage(image) } @concurrent func decodeImage(_ data: Data, at url: URL) async -> Image { Image() } }
-
18:34 - MyImage class with reference semantics
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scale(by factor: Double) { } } let image = MyImage() let otherImage = image // refers to the same object as 'image' image.scale(by: 0.5) // also changes otherImage!
-
19:19 - Concurrently scaling while displaying an image is a data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start func scaleAndDisplay(imageName: String) { let image = loadImage(imageName) Task { @concurrent in image.scaleImage(by: 0.5) } view.displayImage(image) } // Slide content end func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
20:38 - Scaling and then displaying an image eliminates the data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() func scaleAndDisplay(imageName: String) { Task { @concurrent in let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) } } nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
20:54 - Scaling and then displaying an image within a concurrent asynchronous function
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) } nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
21:11 - Scaling, then displaying and concurrently modifying an image is a data race
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) await view.displayImage(image) image.applyAnotherEffect() } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
21:20 - Applying image transforms before sending to the main actor
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scaleImage(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async { let image = loadImage(imageName) image.scaleImage(by: 0.5) image.applyAnotherEffect() await view.displayImage(image) } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }
-
22:06 - Closures create shared state
import Foundation struct Color { } nonisolated class MyImage { var width: Int var height: Int var pixels: [Color] var url: URL init() { width = 100 height = 100 pixels = [] url = URL("https://swift.org")! } func scale(by factor: Double) { } func applyAnotherEffect() { } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() // Slide content start @concurrent func scaleAndDisplay(imageName: String) async throws { let image = loadImage(imageName) try await perform(afterDelay: 0.1) { image.scale(by: 0.5) } await view.displayImage(image) } nonisolated func perform(afterDelay delay: Double, body: () -> Void) async throws { try await Task.sleep(for: .seconds(delay)) body() } // Slide content end nonisolated func loadImage(_ imageName: String) -> MyImage { // decode image return MyImage() } }pet.
-
23:47 - Network manager class
import Foundation nonisolated class MyImage { } struct Connection { func data(from url: URL) async throws -> Data { Data() } } final class NetworkManager { var openConnections: [URL: Connection] = [:] func openConnection(for url: URL) async -> Connection { if let connection = openConnections[url] { return connection } let connection = Connection() openConnections[url] = connection return connection } func closeConnection(_ connection: Connection, for url: URL) async { openConnections.removeValue(forKey: url) } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() let networkManager: NetworkManager = NetworkManager() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let connection = await networkManager.openConnection(for: url) let data = try await connection.data(from: url) await networkManager.closeConnection(connection, for: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> MyImage { // decode image return MyImage() } }
-
25:10 - Network manager as an actor
import Foundation nonisolated class MyImage { } struct Connection { func data(from url: URL) async throws -> Data { Data() } } actor NetworkManager { var openConnections: [URL: Connection] = [:] func openConnection(for url: URL) async -> Connection { if let connection = openConnections[url] { return connection } let connection = Connection() openConnections[url] = connection return connection } func closeConnection(_ connection: Connection, for url: URL) async { openConnections.removeValue(forKey: url) } } final class View { func displayImage(_ image: MyImage) { } } final class ImageModel { var cachedImage: [URL: MyImage] = [:] let view = View() let networkManager: NetworkManager = NetworkManager() func fetchAndDisplayImage(url: URL) async throws { if let image = cachedImage[url] { view.displayImage(image) return } let connection = await networkManager.openConnection(for: url) let data = try await connection.data(from: url) await networkManager.closeConnection(connection, for: url) let image = await decodeImage(data) view.displayImage(image) } @concurrent func decodeImage(_ data: Data) async -> MyImage { // decode image return MyImage() } }
-
-
- 0:00 - 서론
Swift 동시성을 통해 앱은 여러 작업을 동시에 수행하여 응답성을 개선하고 계산을 백그라운드로 오프로드합니다. Swift의 동시성 모델은 동시성 도입을 명시적으로 하고, 동시 작업에서 공유되는 데이터를 식별하며, 컴파일 시점에 잠재적인 데이터 경쟁을 식별함으로써 동시 코드를 올바르게 작성하기 쉽게 해줍니다. 앱은 모든 코드를 메인 스레드에서 실행하여 시작합니다. 복잡성이 증가함에 따라 네트워크 접속과 같은 대기 시간이 긴 작업에 비동기 작업이 도입될 수 있습니다. 백그라운드 스레드는 계산 집약적인 작업에 사용될 수 있습니다. Swift는 이러한 동시 작업을 표현하기 위한 액터 및 작업과 같은 도구를 제공합니다.
- 3:17 - 단일 스레드 코드
Swift에서는 단일 스레드 코드가 메인 액터와 격리된 메인 스레드에서 실행됩니다. 메인 액터에서는 이를 실행할 수 있는 메인 스레드가 하나뿐이기 때문에 동시성이 없습니다. 데이터 또는 코드는 @MainActor 표기를 사용해 메인 액터에서 지정될 수 있습니다. Swift는 메인 액터 코드가 메인 스레드에서만 실행되도록 보장하고 메인 액터 데이터는 해당 스레드에서만 액세스됩니다. 이러한 코드는 메인 액터에 격리됩니다. 기본적으로 Swift는 메인 액터를 사용하여 메인 스레드 코드를 보호함으로써 공유 상태에 자유롭게 액세스할 수 있도록 보장합니다. 메인 액터를 사용한 코드 보호는 기본적으로 빌드 설정에 기반하여 추진됩니다. 이 기능은 주로 주요 앱 모듈과 UI 상호작용에 초점을 맞춘 모듈에 사용합니다.
- 6:00 - 비동기식 작업
Swift 동시성은 async 및 await를 사용하여 함수를 비차단적으로 만들어 네트워크 요청처럼 데이터를 기다리는 동안 다른 작업을 실행할 수 있도록 지원합니다. 이렇게 하면 대기 이벤트 전후에 실행되는 부분으로 함수를 나누어 중단 현상을 방지하고 UI 반응성을 향상시킬 수 있습니다.
- 7:24 - 교차 실행
비동기 함수는 작업에서 실행되고 다른 작업과 독립적으로 실행됩니다. 단일 스레드는 ‘interleaving’을 사용하여 준비가 되면 독립적인 작업을 번갈아 실행할 수 있습니다. 이렇게 하면 유휴 시간을 피하여 성능을 향상시키고 시스템 리소스를 효율적으로 사용할 수 있습니다. 여러 개의 독립적인 작업을 동시에 수행할 때 여러 개의 비동기 작업이 효과적으로 작용합니다. 특정 순서로 작업을 수행할 때는 단일 작업을 사용하세요. 단일 스레드에서 비동기 작업을 사용하면 충분한 경우가 많습니다. 메인 스레드에 너무 많은 부담이 가해지면 Instruments와 같은 프로파일링 툴을 사용하여 동시성을 도입하기 전에 최적화를 위한 병목 현상을 식별하는 데 도움이 될 수 있습니다.
- 10:22 - 동시성 소개
동시성을 사용하면 코드의 일부를 메인 스레드와 병렬로 백그라운드 스레드에서 실행할 수 있기 때문에 시스템의 CPU 코어를 더 많이 사용함으로써 작업을 더 빨리 완료할 수 있습니다. 성능을 개선하기 위해 이 예제에서는 백그라운드 스레드에서 코드를 실행하는 동시성을 도입함으로써 메인 스레드를 해제합니다.
- 11:07 - 동시 함수
@concurrent 속성을 적용하면 Swift가 백그라운드에서 함수를 실행하도록 지시됩니다. Swift 컴파일러는 주요 액터의 데이터 액세스를 강조하여 동시성을 안전하게 도입합니다. 작업이 동기적으로 실행되도록 하는 가장 좋은 방법 중 하나는 주요 액터 코드를 항상 메인 스레드에서 실행되는 호출자로 옮기는 것입니다.
- 13:10 - 비격리 코드
@concurrent 함수는 항상 액터에서 벗어나서 실행됩니다 ‘nonisolated’ 키워드를 사용하면 ・・ main 또는 background 등 클라이언트가 코드를 실행할 위치를 선택할 수 있습니다. 범용 라이브러리의 경우, 격리되지 않은 API를 제공하고 클라이언트가 작업을 오프로드할지 여부를 결정하도록 하는 것이 좋습니다. 더 많은 동시성 옵션을 보려면 WWDC23의 ‘구조화된 동시성의 기초를 넘어’를 참고하세요.
- 14:13 - 동시 스레드 풀
작업을 백그라운드로 오프로드할 때 시스템은 동시 스레드 풀의 스레드에서 작업 일정을 관리합니다. 기기가 작으면 풀에 있는 스레드 수가 적을 수 있지만, 코어가 많은 대형 시스템은 풀에 있는 스레드 수가 더 많습니다. 작업은 풀 내 사용 가능한 스레드에 할당되어 작업이 일시 중단되고 다시 시작됨에 따라 스레드가 변경될 수 있기 때문에 리소스 사용이 최적화됩니다.
- 14:58 - 데이터 공유
여러 스레드 간에 동시성을 사용하고 데이터를 공유하면 공유된 변경 가능 상태에 액세스하기 때문에 런타임 버그가 발생할 위험이 있습니다. Swift의 디자인은 컴파일 타임 검사를 제공하여 이 문제를 완화함으로써 개발자가 더욱 자신 있게 동시 코드를 작성할 수 있도록 지원합니다.
- 15:49 - 값 유형
동시 작업을 처리할 시, 값 유형을 사용하면 상당한 이점을 누릴 수 있습니다. 값 유형이 백그라운드 스레드에 복사되면 독립 복사본이 생성되어 메인 스레드에서 변경된 내용이 백그라운드 스레드의 값에 영향을 미치지 않도록 보장합니다. 이러한 독립성 덕분에 스레드 간에 값 유형을 안전하게 공유할 수 있습니다. ‘Sendable’ 프로토콜을 준수하는 값 유형은 언제나 안전하게 동시 공유할 수 있습니다. 메인 액터 유형은 암묵적으로 Sendable입니다.
- 17:16 - 액터 격리 유형
Swift 액터는 단일 작업 액세스를 보장하여 Sendable이 아닌 상태를 보호합니다. 액터와 값을 주고받을 때 Swift 컴파일러는 안전성을 검증합니다.
- 18:30 - 클래스
Swift에서 클래스는 한 변수를 통해 객체를 변경하면 해당 객체를 가리키는 모든 변수에 변경 사항이 적용된다는 의미의 참조 유형입니다. 여러 스레드가 동시에 동일한 Sendable이 아닌 객체에 액세스하여 수정하면 데이터 경쟁, 충돌 또는 오류가 발생할 수 있습니다. Swift의 동시성 시스템은 Sendable 유형만 액터 및 작업 사이에서 공유되도록 하여 컴파일되는 시점에 이를 방지합니다. 데이터 경쟁을 피하려면 변경 가능한 객체가 동시에 공유되지 않도록 하는 것이 중요합니다. 객체를 다른 작업이나 액터에게 보내 표시 또는 처리하기 전에 객체에 대한 수정을 완료합니다. 객체를 백그라운드 스레드에서 수정해야 하는 경우 객체를 ‘nonisolated’로 설정하지만 Sendable로 설정하지는 않습니다. 공유 상태의 클로저는 동시에 호출되지 않는 한 안전할 수도 있습니다.
- 23:18 - 액터
앱이 커짐에 따라 주요 액터는 다양한 상태를 관리할 수 있어 컨텍스트 전환이 빈번해집니다. 액터를 도입하면 이를 완화할 수 있습니다. 액터는 데이터를 격리하여 한 번에 하나의 스레드만 데이터에 액세스할 수 있도록 하여 경합을 방지합니다. 메인 액터에서 전담 액터로 상태를 전환하면 더 많은 코드를 백그라운드 스레드에서 동시에 실행할 수 있습니다. 이렇게 하면 메인 스레드가 비어 있어서 반응성을 유지할 수 있습니다. UI에 관련된 클래스와 모델 클래스는 일반적으로 메인 액터에 남아 있어야 하거나 전송 불가능하기 때문에 유지되어야 합니다.
- 26:12 - 요약
앱은 단일 스레드로 시작하여 더 나은 성능을 위해 비동기 작업, 동시 코드, 액터를 사용하도록 발전되는 경우가 많습니다. Swift 동시성은 이러한 전환에 도움이 되어 메인 스레드에서 코드를 더 쉽게 옮기고 반응성을 개선할 수 있습니다. Instruments와 같은 프로파일링 툴은 언제 어떤 코드를 메인 스레드에서 옮겨야 하는지 식별하는 데 도움이 됩니다. 권장되는 빌드 설정을 사용하면 동시성 도입을 간소화할 수 있으며, 접근 가능한 동시성 설정을 사용하면 동시성 작업을 보다 쉽게 만들어 주는 일련의 향후 기능을 활성화할 수 있습니다.