스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift Concurrency를 사용하여 데이터 경합 제거
Swift Concurrency의 핵심 개념 중 하나인 Task 격리 및 Actor 격리에 대해 함께 알아보겠습니다. 데이터 경합을 제거하는 Swift의 접근 방식을 소개하고 이것이 앱 아키텍처에 미치는 영향에 대해 안내합니다. 또한 코드에서 원자성의 중요성에 대해 논의하고, 격리를 유지하기 위한 Sendable 점검의 의미를 소개하며, 동시 시스템에서 작업의 순서를 지정하는 것과 관련한 가정을 다시 살펴봅니다.
리소스
관련 비디오
WWDC22
WWDC21
-
다운로드
♪ 부드러운 힙합 음악 ♪ ♪ 안녕하세요 Swift 팀의 Doug입니다 저는 오늘 데이터 레이스를 제거하는 Swift Concurrency의 접근 방식에 대해 이야기합니다 우리는 Swift Concurrency를 동시 프로그램을 더 쉽게 작성하는 일련의 언어 기능으로 소개했습니다 이러한 개별 언어 기능의 메커니즘을 알고 싶다면 각각을 다루는 2021년 WWDC 강연을 참조하세요 이 강연에서는 Swift Concurrency를 데이터 레이스를 도입하지 않고도 동시성을 효율적으로 활용하도록 프로그램을 구성하는 하나의 방법으로 전체적인 관점에서 살펴봅니다 그러려면 훌륭한 비유가 필요할 테니 험난한 동시성의 바다로 여러분을 초대하겠습니다 동시성의 바다는 예측할 수 없으며 여러 일들이 동시에 일어나고 있습니다 하지만 여러분이 키를 잡고 Swift가 항해를 도와준다면 놀라운 것들을 만들어 낼 수 있습니다 함께 뛰어듭시다! 먼저 Swift Concurrency 모델의 핵심 아이디어 중 하나인 격리에 대해 이야기합니다 데이터가 데이터 레이스를 도입할 수 있는 방식으로 공유되지 않도록 보장하죠 작업 격리부터 시작할게요 우리의 동시성 바다에서는 작업이 보트로 표현됩니다 보트는 우리의 주요 일꾼으로 해야 할 일이 있습니다 그들은 처음부터 끝까지 순차적으로 일을 수행하죠 이들은 비동기적이며 코드에서 ‘await’ 작업 시 몇 번이고 중단될 수 있습니다 마지막으로 이들은 독립적입니다 각 작업은 자체 리소스가 있어서 바다에 있는 다른 보트들과 독립적으로 스스로 작동할 수 있습니다 우리 보트가 완전히 독립되어 있다면 데이터 레이스가 없는 동시성이 가능하지만 통신할 방법이 없다면 그다지 유용하지 않습니다 커뮤니케이션을 더해 볼까요? 예를 들어, 한 보트는 다른 보트와 파인애플을 공유하려고 합니다 그래서 보트들이 공해에서 만나고 우리는 파인애플을 한 보트에서 다른 보트로 옮깁니다 여기서 물리적인 비유가 조금 무너집니다 왜냐하면 이 파인애플은 사실 한 보트에서 다른 보트로 이동하는 물리적 물건이 아니거든요 사실은 데이터예요 Swift에서는 데이터를 나타내는 몇 가지 다양한 방법이 있죠 이 파인애플 유형을 어떻게 정의할까요? 우리는 Swift의 값 유형을 좋아하니까 파인애플을 무게와 숙성도로 정의되는 구조체로 만들게요 이게 어떻게 작동하는지 보죠 보트들이 공해에서 만날 때 우리는 사실 한 보트에서 다른 보트로 파인애플 복사본을 전달하고 각 보트는 각자의 복사본을 챙겨서 떠납니다 slice() 및 ripen() 메서드를 호출하여 복사본을 변형하려는 경우 다른 복사본에는 아무런 영향도 미치지 않겠죠 Swift는 바로 이런 이유로 항상 값 유형을 선호해 왔습니다 변형이 국소적인 영향만 미치니까요 이 원리는 값 유형이 격리를 유지하도록 돕습니다 이제 데이터 모델을 조금 더 확장해 닭을 추가해 볼까요? 먹기에만 좋은 파인애플과는 달리 닭은 그들만의 고유한 개성을 가진 아름다운 생명체입니다 따라서 이렇게 클래스로 모델링하도록 하죠 우리 용감한 선원들이 닭을 교환한다고 칩시다 보트가 만나서 닭을 공유합니다 다만 닭과 같은 참조 유형을 복사한다고 해서 닭의 전체 복사본이 되는 것은 아닙니다 이 특정 객체에 대한 참조를 제공하죠 그래서 보트가 각자 갈 길을 가고 나면 문제가 생겼다는 것을 알 수 있습니다 두 보트는 동시에 자신의 일을 하고 있지만 둘 다 같은 닭 객체를 참조하기 때문에 독립적이지 않습니다 이 공유된 가변 데이터는 데이터 레이스에 영향을 잘 받는데 예를 들어 한 보트가 닭에게 먹이를 주려 하고 다른 보트가 닭과 함께 놀고 싶어 하면 닭은 매우 혼란스러워집니다 보트들이 파인애플을 공유하는 건 안전해도 닭은 안전하지 않다는 걸 알 수 있는 방법이 필요합니다 그런 다음 닭들이 우연히 한 보트에서 다른 보트로 옮겨지지 않도록 Swift 컴파일러에 확인 도구를 마련해야 합니다 Swift 프로토콜은 유형을 분류하는 좋은 방법이므로 해당 행동에 대해 추론할 수 있습니다 전송 가능 프로토콜은 데이터 레이스를 생성하지 않고 서로 다른 격리 도메인 간에 안전하게 공유할 수 있는 유형을 설명하는 데 사용됩니다 적합성을 작성하면 유형을 전송 가능으로 만들 수 있죠 파인애플 구조체는 값 유형이기 때문에 전송 가능하지만 닭 클래스는 동기화되지 않은 참조 유형이기 때문에 전송할 수 없습니다 전송 가능을 프로토콜로 모델링하면 격리 도메인 전반에서 데이터를 공유할 위치를 설명할 수 있습니다 예를 들어 작업이 값을 반환할 때 이 값은 해당 값을 기다리는 모든 작업에 제공됩니다 이 경우, 작업에서 닭을 반환하려고 하는데 닭은 전송 가능하지 않아서 안전하지 않다는 오류가 표시됩니다 실제 전송 가능 제약 조건은 성공이라는 작업의 결과 유형이 전송 가능 프로토콜을 준수해야 한다고 지정하는 작업 구조체 자체의 정의에서 비롯됩니다 서로 다른 격리 도메인 간에 값이 전달되는 제네릭 매개 변수가 있는 경우에는 전송 가능 제약 조건을 사용해야 합니다 이제 보트 간에 데이터를 공유하는 아이디어에 대해 다시 살펴봅시다 두 보트가 공해에서 만나 데이터를 공유하려고 할 때 우리는 모든 물품이 공유하기에 안전한지 지속적으로 점검해 줄 누군가가 필요합니다 이게 바로 Swift 컴파일러가 맡은 친절한 세관 검사관의 역할입니다 전송 가능 유형만 교환되도록 확인합니다 파인애플은 전송이 가능하기 때문에 괜찮고 자유롭게 교환할 수 있죠 하지만 닭은 교환이 불가능합니다 친절한 세관 검사관은 우리가 그런 실수를 저지르지 않게 도와줍니다 컴파일러는 여러 지점에서 전송 가능 여부를 확인하는 데 관여합니다 전송 가능 유형은 구성에 따라 정확해야 하며 이들을 통해 공유 데이터가 밀반입되지 않게 합니다 나열자 및 구조체는 일반적으로 모든 인스턴스 데이터를 함께 복사하여 독립적인 값을 생성하는 값 유형을 정의합니다 따라서 모든 인스턴스 데이터가 전송 가능하기만 하면 그들도 전송 가능하죠 전송 가능은 조건 적합성을 사용하여 컬렉션 및 기타 제네릭 유형을 통해서 전파될 수 있습니다 전송 가능 유형의 배열도 전송 가능하므로 파인애플이 가득 찬 상자도 전송 가능합니다 이러한 모든 전송 가능 적합성은 비공개 유형의 경우 Swift 컴파일러에서 유추할 수 있으므로 숙성도와 파인애플 상자는 모두 암시적으로 전송 가능합니다 하지만 닭떼를 수용할 닭장을 만든다고 칩시다 이 유형은 전송 불가능 상태를 포함하므로 전송 가능으로 표시할 수 없습니다 닭은 전송이 불가능하므로 닭의 배열도 전송 불가능하죠 컴파일러에서 이 유형을 안전하게 공유할 수 없음을 알리는 오류 메시지가 나타납니다 클래스가 참조 유형이기 때문에 최종 클래스에 불변 스토리지만 있는 경우처럼 매우 드문 상황에서만 전송이 가능합니다 닭 클래스를 전송 가능 상태로 만들려고 시도하면 가변 상태가 포함되어 있어서 오류가 발생합니다 자, 이제 예를 들어 잠금을 일관되게 사용하여 자체 내부 동기화를 수행하는 참조 유형을 구현할 수 있습니다 이러한 유형은 개념적으로 전송 가능하지만 Swift가 이에 대해 추론할 방법이 없습니다 선택 해제된 전송 가능을 사용해 컴파일러의 검사를 비활성화합니다 단 @unchecked 전송 가능을 통해 가변 상태를 밀반입하면 Swift가 제공하는 데이터 레이스 안전 보장이 손상되므로 이 점에 주의하셔야 합니다 작업 생성에는 보트에서 노 젓는 배를 보내는 것과 같이 새로운 독립적인 작업에서 클로저를 실행하는 게 포함됩니다 이렇게 하면 원래 작업에서 값을 캡처하여 새 작업으로 전달할 수 있으므로 데이터 레이스를 도입하지 않도록 전송 가능 검사가 필요합니다 전송 불가능 유형을 이 경계를 넘어 공유하려고 하면 Swift 컴파일러에서 이런 오류 메시지를 생성하여 문제를 해결해 줍니다 이건 작업 생성의 마법이 아닙니다 클로저는 전송 가능 클로저로 추정되며 이는 At-Sendable로 명시적으로 작성됐을 수 있죠 전송 가능 클로저는 전송 가능 함수 유형의 값입니다 At-Sendable은 함수 유형에 쓰여 함수 유형이 전송 가능 프로토콜을 준수함을 나타낼 수 있습니다 즉, 캡처된 상태에서 데이터 레이스를 도입하지 않고도 해당 함수 유형의 값을 다른 격리 도메인으로 전달하여 거기서 호출할 수 있습니다 일반적으로 함수 유형은 프로토콜을 준수할 수 없지만 전송 가능은 컴파일러가 해당 함수에 대한 의미 요구 사항을 검증하기 때문에 특별합니다 전송 가능 프로토콜을 준수하는 전송 가능 유형의 튜플을 위한 유사한 지원이 있습니다 이를 통해 전송 가능을 전체 언어로 사용할 수 있습니다 앞에서 설명한 시스템에는 서로 격리된 동시 실행 작업이 많습니다 전송 가능 프로토콜은 작업 간에 안전하게 공유할 수 있는 유형을 설명하고 Swift 컴파일러는 작업 격리를 유지하기 위해 전송 가능 적합성을 모든 수준에서 확인합니다 그러나 공유된 가변 데이터에 대한 개념이 없으면 작업을 의미 있는 방식으로 조율하기 어렵습니다 따라서 데이터 레이스를 다시 도입하지 않는 작업 간에 데이터를 공유할 방법이 필요합니다 여기서 바로 행위자가 나타납니다 행위자는 서로 다른 작업을 통해 액세스할 수 있는 상태를 격리할 방법을 제공하는데 데이터 경주를 제거하는 조정된 방법으로 제공합니다 행위자는 동시성 바다에 있는 섬들입니다 보트처럼 각 섬은 독립적이며 바다의 모든 것들로부터 격리되어 그들만의 상태를 가지고 있죠 이 상태에 액세스하려면 코드가 섬에서 실행 중이어야 합니다 예를 들어 advanceTime 메서드는 이 섬으로 격리됩니다 이 메서드는 섬에 살고 섬의 모든 상태에 접근할 수 있습니다 섬에서 실제로 코드를 실행하려면 보트가 필요합니다 보트는 섬에서 코드를 실행하도록 섬을 방문할 수 있으며 이때 보트는 해당 상태에 액세스할 수 있습니다 한 번에 보트 하나만 섬을 방문해 코드를 실행할 수 있으므로 섬 상태에 동시에 액세스하는 일이 없도록 보장합니다 만약 다른 보트가 나타난다면 그들은 섬을 방문할 차례를 기다려야 합니다 이 보트가 섬을 방문할 기회를 얻기까지는 오랜 시간이 걸릴 수 있으므로 행위자에 들어가는 것은 await 키워드로 표시된 잠재적인 중단 지점입니다 일단 섬이 다시 한번 중단 지점에서 해방되면 다른 보트가 방문할 수 있죠 공해에서 만나는 두 보트처럼 보트와 섬 사이의 상호 작용은 전송 불가능 유형이 둘 사이를 통과하지 않도록 하여 서로의 격리를 유지해야 합니다 예를 들어 우리는 섬의 닭떼에 보트에 있는 닭 한 마리를 추가하려고 시도할 수 있죠 그러면 서로 다른 격리 도메인에서 동일한 닭 개체에 대한 참조가 두 개 생성되므로 Swift 컴파일러는 이를 거부합니다 마찬가지로 우리가 섬에서 반려용 닭을 입양하여 보트에 싣고 가려고 하면 전송 가능 검사 때문에 이 데이터 레이스를 만들 수 없습니다 행위자는 참조 유형이지만 클래스와 달리 모든 속성과 코드를 격리하여 동시 액세스를 방지합니다 따라서 다른 격리 도메인의 행위자를 참조하는 것이 안전합니다 섬으로 가는 지도를 가진 것과 같죠 섬을 방문하기 위해 그 지도를 사용할 수 있지만 그 상태에 액세스하려면 여전히 도킹 절차를 거쳐야 합니다 따라서 모든 행위자 유형은 암묵적으로 전송 가능입니다 어떤 코드가 행위자에게 격리되어 있고 어떤 코드가 그렇지 않은지 어떻게 알 수 있을까요? 행위자의 격리는 여러분의 맥락에 따라 결정됩니다 행위자의 인스턴스 속성이 해당 행위자로 분리됩니다 이 advanceTime 메서드처럼 기본값으로 행위자 또는 행위자 확장 프로그램의 인스턴스 메서드도 분리되죠 축소 알고리즘으로 전달된 클로저와 같이 전송 불가능한 클로저는 행위자에 머무르며 행위자 격리 맥락에 있을 때 행위자 격리됩니다 작업 이니셜라이저는 또한 맥락에서 행위자 분리를 상속하므로 생성된 작업이 처음 시작된 행위자와 동일한 행위자에 대해 예약됩니다 여기는 닭떼에 대한 액세스를 허용합니다 반면에 분리된 작업은 맥락에서 행위자 격리를 상속하지 않습니다 그것이 만들어진 맥락과 완전히 독립적이기 때문입니다 보시다시피 여기 클로저에 있는 코드는 행위자 외부에 있는 것으로 간주됩니다 격리된 음식 속성을 참조하려면 await를 사용해야 하기 때문이죠 이 클로저를 위한 용어가 있으니 비격리 코드입니다 비격리 코드는 어떤 행위자에게도 실행되지 않는 코드입니다 비격리 키워드를 사용하여 행위자의 외부에 배치하면 행위자의 내부에 있는 기능을 명시적으로 비격리로 만들 수 있습니다 분리된 작업에 사용된 클로저에서 암시적으로 발생한 상황과 같습니다 즉, 행위자에게 격리된 상태를 읽고 싶다면 섬으로 가서 필요한 상태의 복사본을 얻기 위해 await를 사용해야 합니다 비격리 비동기 코드는 항상 글로벌 협동 풀에서 실행됩니다 보트가 공해에 나와 있을 때만 작동한다고 생각해 보세요 그러면 일을 하려면 방문하는 섬을 떠나야겠죠 따라서 전송 불가능 데이터를 가지고 있지는 않은지 확인해야 합니다! 여기서 컴파일러는 전송 불가능한 닭의 인스턴스가 섬을 떠나려고 하는 잠재적 데이터 레이스를 탐지합니다 격리되지 않은 코드의 경우를 하나 더 고려해 보겠습니다 인사 작업은 비격리 비동기 코드입니다 전반적으로 보트나 섬 또는 동시성에 대해 아무것도 모르죠 여기서 이걸 행위자 격리된 greetOne 함수로부터 호출하고 있습니다 물론 괜찮습니다! 이 동기 코드는 섬에서 호출되면 섬에 남기 때문에 닭떼의 닭에서 자유롭게 실행될 수 있습니다 반면 우리에게 인사라는 비격리 비동기 작업이 있다면 이 인사는 공해 바다에 있는 보트에서 실행될 겁니다 Swift 코드는 대부분 이렇습니다 동기화되어 있으며 모든 행위자와 격리되지 않고 주어진 매개 변수에서만 작동하기 때문에 이 코드는 호출되는 격리 도메인에 머무릅니다 행위자들은 프로그램의 나머지 부분과 격리된 상태를 유지합니다 행위자에 대해 한 번에 하나의 작업만 실행할 수 있으므로 해당 상태에 대한 동시 액세스는 없습니다 전송 가능 검사는 작업이 행위자를 출입시킬 때마다 적용되어 비동기 가변 상태가 빠져나가지 않게 합니다 이를 모두 합쳐 행위자를 Swift의 동시 프로그램에서 구성 요소 중 하나로 만듭니다 우리가 자주 말하는 주 행위자라는 특별한 행위자도 있습니다 주 행위자는 바다 한가운데 있는 큰 섬이라고 생각하세요 사용자 인터페이스에 대한 모든 드로잉 및 상호 작용이 발생하는 기본 스레드를 나타냅니다 여러분이 무언가를 드로잉하고 싶다면 주 행위자의 섬에서 코드를 실행해야 합니다 사용자 UI에 매우 중요해서 아예 섬을 ‘UI섬’으로 불러야 할지도 모릅니다 주 행위자가 크다고 말할 때 우리가 의미하는 것은 프로그램의 사용자 인터페이스와 관련된 많은 상태를 포함한다는 뜻입니다 UI 프레임워크와 앱 모두에서 실행해야 하는 코드가 정말 많습니다 하지만 여전히 행위자여서 한 번에 하나의 작업만 실행합니다 그러니 주 행위자에게 너무 많은 작업을 시키거나 오래 걸리는 작업을 주지 않도록 주의하세요 UI가 응답하지 않을 수 있습니다 주 행위자에 대한 격리는 MainActor 속성으로 표현됩니다 이 속성은 코드가 주 행위자에서 실행되어야 함을 나타내기 위해 함수 또는 클로저에 적용될 수 있습니다 그러면 이 코드가 주 행위자에게 격리되어 있다고 말합니다 Swift 컴파일러는 메인 스레드에서만 주 행위자 격리된 코드가 실행되도록 보장하며 다른 행위자에 대한 상호 배타적 액세스를 보장하는 동일한 메커니즘을 사용합니다 주 행위자와 격리되지 않은 맥락에서 updateView를 호출하는 경우 주 행위자로의 전환을 설명하기 위한 await를 도입해야 합니다 주 행위자 속성은 유형에도 적용될 수 있으며 이 경우 해당 유형의 인스턴스는 주 행위자에게 격리됩니다 다시 말하지만 이는 다른 행위자와 똑같습니다 속성은 주 행위자에 있는 동안에만 접근할 수 있고 메서드는 명시적으로 벗어나지 않는 한 주 행위자에게 격리됩니다 일반 행위자와 마찬가지로 주 행위자 클래스에 대한 참조 자체도 데이터가 격리되어 있어 전송 가능합니다 이렇게 하면 UI 뷰 및 뷰 컨트롤러에 주 행위자 주석이 적합해지며 이는 프레임워크 자체에 의해 메인 스레드에 연결되어야 합니다 프로그램의 다른 작업 및 행위자와 뷰 컨트롤러에 대한 참조를 공유할 수 있으며 행위자가 비동기적으로 뷰 컨트롤러에 다시 호출하여 결과를 게시할 수 있습니다 이는 앱의 아키텍처에 직접적인 영향을 미치죠 여러분의 앱에서 여러분의 뷰와 뷰 컨트롤러는 주 행위자에 있을 겁니다 다른 프로그램 로직은 다른 행위자를 사용하여 공유 상태를 안전하게 모델링하고 작업을 사용하여 독립적인 작업을 설명하기 위해 주 행위자와 격리되어야 합니다 또 이러한 작업들은 필요에 따라 주 행위자와 다른 행위자 사이를 왕복할 수 있습니다 동시 앱에서는 많은 작업이 이뤄지므로 사용자가 이해할 수 있도록 몇 가지 훌륭한 도구를 만들었습니다 자세한 내용을 확인하려면 ’Swift Concurrency 시각화 및 최적화’ 강연을 참조하세요 더 깊은 바다로 들어가 원자성에 대해 다뤄 보죠 Swift Concurrency 모델의 목표는 데이터 레이스를 제거하는 것입니다 즉, 데이터 손상을 수반하는 낮은 수준의 데이터 레이스를 제거한다는 의미입니다 여러분은 여전히 원자성에 대해 높은 수준에서 추론해야 합니다 앞서 이야기했듯이 행위자들은 한 번에 하나의 작업만 수행합니다 그러나 행위자에 대한 실행을 중지하면 해당 행위자는 다른 작업을 실행할 수 있습니다 이렇게 하면 프로그램이 진행되므로 교착 상태의 가능성이 제거됩니다 그러나 await 구문에 대한 행위자의 불변을 주의 깊게 고려해야 합니다 그렇지 않으면 실제로 데이터가 손상되지 않더라도 프로그램이 예기치 않은 상태에 있는 높은 수준의 데이터 레이스가 발생할 수 있습니다 이걸 예로 한번 들어 보죠 우리는 여기서 섬에 파인애플을 몇 개 더 저장하려는 함수가 있습니다 행위자의 외부에 있으므로 비격리된 비동기 코드입니다 다시 말해 공해에서 흐른다는 의미입니다 이 함수는 파인애플 몇 개와 이 파인애플을 보관해야 하는 섬 지도도 받았습니다 여기서 첫 번째 흥미로운 작업은 섬에서 나온 음식 배열의 복사본을 얻는 겁니다 그러려면 보트는 wait 키워드로 신호를 받은 섬에 방문해야 합니다 음식의 복사본을 얻는 즉시 보트는 바다로 다시 돌아가 하던 일을 계속합니다 이는 파인애플 매개 변수의 파인애플을 섬에서 가져온 2개에 더한다는 의미입니다 이제 함수의 마지막 줄로 이동하겠습니다 이제 우리 보트는 3개의 파인애플에 섬의 식량 배열을 맞추려고 섬을 다시 방문해야 합니다 이제 모든 게 잘 해결됐습니다 우리는 섬에 파인애플을 3개 가지고 있죠! 하지만 상황이 조금 달라졌을 수도 있습니다 첫 번째 보트가 섬을 방문할 차례를 기다리는 동안 해적선이 들어와 파인애플을 모두 훔쳐 갔다고 가정해 보죠 우리의 원래 보트는 섬에 파인애플 3개를 저장했는데 우리는 문제를 발견했죠 파인애플 3개가 갑자기 파인애플 5개로 바뀌었습니다! 어떻게 된 일일까요? 보시다시피 같은 행위자의 상태에 액세스하려는 await가 2개 있으며 우리는 이 섬의 식량 배열이 이 2개의 await 사이에서 변하지 않는다고 가정하고 있습니다 하지만 이들은 await이므로 행위자가 해적과 싸우는 것과 같은 다른 우선 순위가 높은 일을 하는 동안 우리 작업이 여기서 보류될 수도 있습니다 이 특정한 경우에 Swift 컴파일러는 다른 행위자의 상태를 대놓고 수정하려는 시도를 거부합니다 하지만 우리는 저장 작업을 행위자에 대한 동기 코드로 다시 써야 합니다 바로 이렇게요 이것은 동기식 코드이므로 중단 없이 행위자에서 실행될 겁니다 따라서 우리는 남들이 모든 함수를 통해 섬의 상태를 변경하지 않을 것으로 확신할 수 있습니다 행위자를 작성할 때는 어떤 식으로든 인터리빙이 가능한 동기식 트랜잭션 작업 측면에서 생각해 보세요 이들 모두는 행위자가 떠날 때 좋은 상태에 있는지 확인해야 합니다 비동기 행위자 작업의 경우 간단하게 유지하세요 주로 동기식 트랜잭션 작업으로부터 구성하고 각 await 작업 시 행위자의 상태가 양호하도록 주의해야 합니다 이렇게 하면 행위자를 최대한 활용하여 하위 수준과 상위 수준 데이터 레이스를 모두 제거할 수 있습니다 동시 실행 프로그램에서는 여러 가지 일들이 동시에 발생하므로 이러한 일들이 일어나는 순서는 실행마다 다를 수 있습니다 그러나 프로그램은 이벤트를 일관된 순서로 처리하는 데 의존하는 경우가 많습니다 예를 들어 사용자 입력 또는 서버의 메시지에서 나오는 이벤트 스트림이 있습니다 이러한 이벤트 스트림이 들어오면 우리는 그 효과가 순서대로 나타날 것으로 예상합니다 Swift Concurrency는 작업 순서를 지정하기 위한 도구를 제공하지만 행위자는 이를 위한 도구가 아닙니다 행위자는 전체 시스템의 응답성을 유지하기 위해 가장 우선 순위가 높은 작업을 먼저 실행합니다 이렇게 하면 우선 순위가 낮은 작업이 동일한 행위자에서 우선순위가 높은 작업보다 먼저 발생하는 우선 순위 역전이 제거되죠 이는 완전히 선입선출 순서로 실행되는 직렬 디스패치 큐와는 상당한 차이가 있습니다 Swift Concurrency에는 작업 주문을 위한 몇 가지 도구가 있죠 첫 번째는 우리가 이미 많이 이야기한 작업입니다 작업은 여러분이 익숙한 일반적인 제어 흐름으로 처음부터 끝까지 실행됩니다 자연스럽게 작업을 주문하죠 AsyncStream으로 실제 이벤트 스트림을 모델링할 수 있습니다 하나의 작업은 for-await-in 루프를 통해 이벤트 스트림을 반복하여 차례로 각 이벤트를 처리할 수 있습니다 AsyncStream은 순서를 유지하면서도 스트림에 요소를 추가할 수 있는 이벤트 생성자와 얼마든지 공유될 수 있습니다 Swift의 동시성 모델이 작업 및 행위자 경계에서 전송 가능 검사를 통해 유지되는 격리 개념을 사용하여 데이터 레이스를 제거하는 방법에 대해 많은 이야기를 나눴습니다 그러나 모든 전송 가능 유형을 모든 곳에서 표시하기 위해 하고 있는 작업을 모두 중지할 수는 없습니다 대신 점진적인 접근이 필요합니다 Swift 5.7은 Swift 컴파일러가 얼마나 엄격하게 전송 가능성을 검사해야 하는지 지정하는 빌드 설정을 도입했습니다 기본값 설정은 최소입니다 즉, 컴파일러는 사용자가 명시적으로 무언가를 전송 가능으로 표시하려고 시도한 위치만 진단합니다 이는 Swift 55 및 56의 동작과 유사하며 위의 경우 경고나 오류가 발생하지 않습니다 이제 전송 가능 호환성을 추가하면 컴파일러는 닭이 전송 불가능하기 때문에 닭장 유형도 전송 불가능하다고 문제를 제기할 겁니다 그러나 이 문제와 기타 전송 가능 관련 문제는 Swift 5에서 오류가 아닌 경고로 표시되므로 문제를 하나하나 더 쉽게 해결할 수 있습니다 데이터 레이스의 안전성을 향상시키려면 표적화된 엄격한 동시성 설정을 활성화하세요 이 설정을 사용하면 async/await, 작업 또는 행위자 같은 Swift Concurency 기능을 이미 채택한 코드에 대한 전송 가능 검사를 활성화합니다 예를 들어 새로 만든 작업에서 전송 불가능 유형의 값을 캡처하려는 시도를 식별하죠 전송 불가능 유형이 다른 모듈에서 나오는 경우도 간혹 있습니다 전송 가능에 대해 아직 업데이트되지 않은 패키지거나 사용자가 아직 손보지 못한 자체 모듈일 수도 있습니다 이러한 경우 @preconcurrency 속성을 사용하여 해당 모듈에서 오는 유형에 대해 일시적으로 전송 가능 경고를 비활성화할 수 있습니다 그러면 이 소스 파일 내의 닭 유형에 대한 전송 가능 경고가 음소거됩니다 어느 시점에서 FarmAnimals 모듈이 전송 가능 적합성으로 업데이트됩니다 이어서 다음 두 가지 중 하나가 발생합니다 첫째, 닭이 어떻게든 전송 가능해집니다 이 경우 preconcurrency 속성을 가져오기에서 제거할 수 있습니다 둘째, 닭은 전송 불가능으로 알려집니다 이 경우 경고가 다시 나타나 닭이 전송 가능하다는 가정이 사실은 올바르지 않다고 나타냅니다 표적화된 엄격성 설정은 기존 코드와의 호환성과 잠재적인 데이터 레이스를 식별하는 것 사이에서 균형을 맞추려고 합니다 하지만 만약 여러분이 레이스가 일어날 수 있는 모든 위치를 보고 싶다면 한 가지 다른 방법이 있으니 바로 전체 검사입니다 전체 검사는 의도된 Swift 6의 의미와 근사하여 데이터 레이스를 완전히 제거합니다 이전 두 모드에서 확인하는 모든 항목을 검사하지만 모듈의 모든 코드에 대해 이를 수행합니다 여기서는 사실 Swift의 동시성 기능을 전혀 사용하지 않습니다 오히려 디스패치 큐에서 작업을 수행하며 이 작업은 해당 코드를 동시에 실행합니다 디스패치 큐의 비동기 작업은 실제로 전송 가능 클로저를 취한다고 알려져 있죠 따라서 컴파일러는 디스패치 큐에서 전송 불가능 본문이 실행 중인 코드에 의해 캡처될 때 데이터 레이스가 있음을 나타내는 경고를 생성합니다 본문 매개 변수를 전송 가능으로 만들면 이를 수정할 수 있습니다 이렇게 변경하면 이 경고가 제거되고 이제 doWork의 모든 호출자는 전송 가능 클로저를 제공해야 한다는 것을 알게 됩니다 즉, 데이터 레이스를 더 잘 검사할 수 있으며 이제 방문 함수가 데이터 레이스의 소스라는 것을 알 수 있죠 전체 검사를 완료하면 프로그램의 잠재적인 데이터 레이스를 플러시할 수 있습니다 데이터 레이스를 제거하겠다는 Swift의 목표를 달성하려면 결국 검사를 완료해야 합니다 목표를 향해 점진적으로 노력할 것을 권장합니다 Swift의 동시성 모델을 채택하여 데이터 레이스 안전을 위해 앱을 설계한 다음 점진적으로 더 엄격한 동시성 검사를 활성화하여 코드에서 오류 클래스를 제거합니다 가져온 유형에 대한 경고를 억제하기 위해 @preconcurrency로 가져오기를 표시하면서 초조해하지 마세요 이 모듈들은 더 엄격한 동시성 검사를 채택하므로 컴파일러는 여러분의 가정을 재점검할 겁니다 이 과정을 마치면 코드는 메모리 안전성과 데이터 레이스 안전성의 이점을 모두 누릴 수 있으므로 여러분은 훌륭한 앱을 구축하는 데 집중할 수 있죠 저와 함께 동시성의 바다를 항해해 주셔서 감사합니다 ♪
-
-
1:18 - Tasks
Task.detached { let fish = await catchFish() let dinner = await cook(fish) await eat(dinner) }
-
2:31 - What is the pineapple?
enum Ripeness { case hard case perfect case mushy(daysPast: Int) } struct Pineapple { var weight: Double var ripeness: Ripeness mutating func ripen() async { … } mutating func slice() -> Int { … } }
-
3:15 - Adding chickens
final class Chicken { let name: String var currentHunger: HungerLevel func feed() { … } func play() { … } func produce() -> Egg { … } }
-
4:35 - Sendable protocol
protocol Sendable { }
-
4:44 - Use conformance to specify which types are Sendable
struct Pineapple: Sendable { … } //conforms to Sendable because its a value type class Chicken: Sendable { } // cannot conform to Sendable because its an unsynchronized reference type.
-
4:57 - Check Sendable across task boundaries
// will get an error because Chicken is not Sendable let petAdoption = Task { let chickens = await hatchNewFlock() return chickens.randomElement()! } let pet = await petAdoption.value
-
5:26 - The Sendable constraint is from the Task struct
struct Task<Success: Sendable, Failure: Error> { var value: Success { get async throws { … } } }
-
6:23 - Sendable checking for enums and structs
enum Ripeness: Sendable { case hard case perfect case mushy(daysPast: Int) } struct Pineapple: Sendable { var weight: Double var ripeness: Ripeness }
-
6:52 - Sendable checking for enums and structs with collections
//contains an array of Sendable types, therefore is Sendable struct Crate: Sendable { var pineapples: [Pineapple] }
-
7:17 - Sendable checking for enums and structs with non-Sendable collections
//stored property 'flock' of 'Sendable'-conforming struct 'Coop' has non-sendable type '[Chicken]' struct Coop: Sendable { var flock: [Chicken] }
-
7:36 - Sendable checking in classes
//Can be Sendable if a final class has immutable storage final class Chicken: Sendable { let name: String var currentHunger: HungerLevel //'currentHunger' is mutable, therefore Chicken cannot be Sendable }
-
7:58 - Reference types that do their own internal synchronization
//@unchecked can be used, but be careful! class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable { var lock: NSLock var storage: [Key: Value] }
-
8:21 - Sendable checking during task creation
let lily = Chicken(name: "Lily") Task.detached {@Sendable in lily.feed() }
-
9:08 - Sendable function types
struct Task<Success: Sendable, Failure: Error> { static func detached( priority: TaskPriority? = nil, operation: @Sendable @escaping () async throws -> Success ) -> Task<Success, Failure> }
-
10:28 - Actors
actor Island { var flock: [Chicken] var food: [Pineapple] func advanceTime() }
-
11:03 - Only one boat can visit an island at a time
func nextRound(islands: [Island]) async { for island in islands { await island.advanceTime() } }
-
11:34 - Non-Sendable data cannot be shared between a task and actor
//Both examples cannot be shared await myIsland.addToFlock(myChicken) myChicken = await myIsland.adoptPet()
-
12:43 - What code is actor-isolated?
actor Island { var flock: [Chicken] var food: [Pineapple] func advanceTime() { let totalSlices = food.indices.reduce(0) { (total, nextIndex) in total + food[nextIndex].slice() } Task { flock.map(Chicken.produce) } Task.detached { let ripePineapples = await food.filter { $0.ripeness == .perfect } print("There are \(ripePineapples.count) ripe pineapples on the island") } } }
-
14:03 - Nonisolated code
extension Island { nonisolated func meetTheFlock() async { let flockNames = await flock.map { $0.name } print("Meet our fabulous flock: \(flockNames)") } }
-
14:48 - Non-isolated synchronous code
func greet(_ friend: Chicken) { } extension Island { func greetOne() { if let friend = flock.randomElement() { greet(friend) } } }
-
15:15 - Non-isolated asynchronous code
func greet(_ friend: Chicken) { } func greetAny(flock: [Chicken]) async { if let friend = flock.randomElement() { greet(friend) } }
-
17:01 - Isolating functions to the main actor
@MainActor func updateView() { … } Task { @MainActor in // … view.selectedChicken = lily } nonisolated func computeAndUpdate() async { computeNewValues() await updateView() }
-
17:38 - @MainActor types
@MainActor class ChickenValley: Sendable { var flock: [Chicken] var food: [Pineapple] func advanceTime() { for chicken in flock { chicken.eat(from: &food) } } }
-
19:58 - Non-transactional code
func deposit(pineapples: [Pineapple], onto island: Island) async { var food = await island.food food += pineapples await island.food = food }
-
20:56 - Pirates!
await island.food.takeAll()
-
21:57 - Modify `deposit` function to be synchronous
extension Island { func deposit(pineapples: [Pineapple]) { var food = self.food food += pineapples self.food = food } }
-
23:56 - AsyncStreams deliver elements in order
for await event in eventStream { await process(event) }
-
25:02 - Minimal strict concurrency checking
import FarmAnimals struct Coop: Sendable { var flock: [Chicken] }
-
25:21 - Targeted strict concurrency checking
@preconcurrency import FarmAnimals func visit(coop: Coop) async { guard let favorite = coop.flock.randomElement() else { return } Task { favorite.play() } }
-
26:53 - Complete strict concurrency checking
import FarmAnimals func doWork(_ body: @Sendable @escaping () -> Void) { DispatchQueue.global().async { body() } } func visit(friend: Chicken) { doWork { friend.play() } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.