본문 바로가기

Swift/Concurrency

WWDC 2022 - Eliminate data races using Swift Concurrency

이번 세션의 주제는 data race 문제를 발생시키지 않고 효율적인 동시성 프로그램을 구성하는 전체론적인 Swift Concurrency의 관점을 확인할 것이다.

 

시작에 앞서 진행자는 concurrency를 바다에 비유한다.

 

바다에는 곳곳에서 여러 가지 일이 한 번에 진행되어 예측하기 어렵지만, Swift와 함께 신이 되어 바다를 탐색하면, 놀라운 일을 만들어 낼 수 있다고 한다.

 

그럼 신이 되어 보자.

Task isolation

Sequential - Concurrency 바다에서 task는 보트다. 즉 main workers이다. 그들은 해야 할 일이 있고 순차적으로 처음부터 끝까지 작업을 수행한다. 

Asynchronous - 작업은 비동기적으로 수행되고, 어느 순간이던지 await 키워드를 통해서 일시중지될 수 있다.

Self-contained - 또한 독립적이다. 각 보트(작업)들은 자체적인 리소스가 있어 바다에서 각자 독립적으로 작업을 실행할 수 있다.

 

만약, 보트들이 완벽하게 독립적이라면 concurrency without data races을 가진다. 그러나 통신 방법이 없으면 크게 유용하지 않다.

 

그렇다면 통신 방법을 추가해보자!

한 보트에서 파인애플을 가지고 있고 다른 보트에 공유하고 싶다. 이를 위해 바다에서 보트들은 만나서 파인애플을 한 보트에서 다른 보트로 파인애플을 나눠준다.

여기서 현실에서의 비유가 약간 깨진다. Swift에서 파인애플은 한 보트에서 다른 보트로 옮겨지는 물리적인 물건이 아니고 데이터 타입이기 때문이다.

 

Swift에서는 이 데이터를 나타내는 몇 가지 방법이 있는데, 주로 값타입을 선호하기 때문에 무게와 숙성 상태를 정의한 struct타입의 파인애플을 만들어 볼 것이다.

위와 같이 struct타입으로 만든 파인애플을 공유하는 방법을 보자.

 

보트들이 바다에서 만나는 경우, 파인애플의 인스턴스를 복사하여 다른 보트에 사본을 전달한다. 그리고 각 보트들은 각자의 복사본을 가지고 떠난다.

 

각자 가지고 떠난 파인애플에 자르거나 익히는 수정이 있어도, 그것은 다른 파인애플에 영향을 주지는 않는다.

파인애플은 value type이기 때문에 복사될 때, 데이터의 값이 전달되며, 서로 다른 인스턴스를 가지게 된다.

따라서 값을 변경하는 경우 다른 인스턴스에는 영향을 주지 않는다. 이러한 특성 덕분에 value type은 서로 독립적인 상태를 유지할 수 있다.

 

 

이번에는 닭을 추가해 볼 것이다! 파인애플과 달리 닭은 각자의 성격이 있는 동물이므로, class 타입으로 모델링할 것이다.

 

이 클래스 타입의 닭을 교환하는 상황을 만들어 볼 것이다. 두 보트가 만났을 때, 닭을 나눌 것이다.

 

그러나, 클래스 타입의 닭을 복사하면 참조 타입의 복사만 이루어지기 때문에, 새로운 객체가 생성되지 않는다.

따라서, 보트들이 떠나 각자의 작업을 동시에 실행하면 같은 닭 객체를 두 보트에서 참조하고 있기 때문에 문제가 발생한다.

이러한 공유한 mutable data는 data races의 위험이 있다. 예를 들어, 한 보트에서 닭에게 밥을 주고 다른 배에서는 놀아주려고 할 때, 닭은 혼란스러워진다.

 

우리는 보트 사이에서 파인애플과 닭을 공유하는 것이 안전한지 구분할 수 있는 방법이 필요하다. 또한 Swift 컴파일러에서도 이를 확인할 수 있는 기능이 필요하다.

 

Sendable 프로토콜은 data race를 만들지 않고 서로 다른 isolation domain(격리영역 - 즉 독립적으로 실행되는 코드 블록)들 간에 안전하게 공유할 수 있는 타입을 설명하는 데 사용한다.

타입에 Sendable을 명시함으로써 Sendable을 준수할 수 있다. 파인애플은 struct(값 타입)이기 때문에 Sendable을 준수하지만, 닭은 class(비동기화된 참조 타입)이기 때문에 준수할 수 없다.

 

Sendable 프로토콜을 통해 모델링을 하면 데이터가 격리 영역들 간에 공유되는 곳을 구분할 수 있다.

예를 들어, task가 어떤 값을 return 한다고 할 때 아래와 같이 chicken을 task에서 return 하려고 하면 Sendable 하지 않기 때문에 에러가 발생한다.

 

Task 구조체는 Suceess라는 결과 타입이 Sendable을 준수해야 한다고 정의한다.

Sendable은 격리 영역들 간 전달될 제네릭 파라미터가 있는 경우에 사용해야 한다.

 

다시 보트 간 데이터를 공유하는 상황으로 돌아가보자.

 

두 보트가 바다에서 만나 데이터를 공유하려 한다. 이때 우린 공유하는 데이터들이 공유하기에 안전하지 체크를 해줄 누군가가 필요하다.

이때 Swift 컴파일러는 Sendable 타입의 데이터만 교환되도록 한다.

파인애플은 Sendable을 준수하기 때문에 자유롭게 교환할 수 있으나, 닭을 교환하려는 경우 불가능하다고 컴파일러가 알려준다.

 

컴파일러는 Sendable의 정확성을 다양한 지점에서 확인한다.

Sendable은 구성시 정확해야 하며, 공유 데이터가 그 중간에 잠복할 수 없다.

enum과 struct는 일반적으로 값 타입을 정의하며, 모든 인스턴스 데이터를 함께 복사하여 독립적인 값을 생성한다. 따라서 모든 인스턴스 데이터가 Sendable 하다면, 이들은 Sendable 하다.

그리고 Sendable은 제네릭 타입이 프로토콜을 만족하는 경우, 그 제네릭 타입을 담은 다른 복합 타입들(Collection) 또한 Sendable이 전파될 수 있다.

그러나 Sendable 하지 않은 타입을 포함하고 있다면, Sendable를 명시할 수 없다. 이때 컴파일러에서 오류메시지가 표시된다.

Class는 참조 타입이기 때문에, final class가 immutable storage만 가지고 있는 경우와 같은 매우 제한된 경우에만 Sendable로 만들 수 있다.

만약 lock을 사용해 내부에 동기화를 수행하는 참조 타입을 구현하면 개념적으론 Sendable일 수 있지만, Swift가 이를 추론할 수 있는 방법이 없다.

 

이때 @unchecked Sendable 을 사용해 컴파일러의 검사를 피할 수 있다. 당연히 Sendable 하지 않은 객체를 @unchecked Sendable을 통해 강제로 처리하면 문제가 생길 수 있다.

Task의 생성은 자신의 보트에서 노젓는 보조 배를 보내는 것과 같이 독립적인 새로운 작업에서 클로저를 실행하는 것을 의미한다.

이때 원래의 Task에서 값을 캡처하고 새로운 Task에 전달할 수 있으므로, data race를 발생시키지 않도록 Sendable을 검사할 필요가 있다.

만약 이 경계를 넘어서 non-Sendable을 공유하려 하면, 컴파일러는 이와 관련된 오류 메시지를 생성한다.

 

이것이 가능한 이유는 Task 클로저에 Sendable이 명시되어 있기 때문이다.

 

@Sendable은 메서드 타입에서 작성될 수 있으며, Sendable 프로토콜을 준수함을 나타낸다.

이는 해당 함수 타입의 값이 다른 격리 영역에 전달되어 그곳에서 호출될 때 캡처된 상태에서 데이터 경쟁을 발생시키지 않고 사용될 수 있음을 의미한다.

일반적으로 함수 타입은 프로토콜을 준수할 수 없지만, Sendable은 컴파일러가 의미적으로 요구사항을 확인하기 때문에 특별하다.

Sendable은 Swift 언어 전반에 걸쳐서 사용될 수 있도록 튜플의 Sendable 유형도 프로토콜을 준수하도록 구현되어 있다.

 

 

지금까지 설명으로 Sendable 프로토콜은 안전하게 공유할 수 있는 데이터 타입을 설명한다.

컴파일러는 이러한 Sendable 타입이 다른 격리 구역으로 전달될 때마다 이것이 Sendable을 준수하는지 확인하며 서로 다른 격리 구역에서 작업하는 여러 Task가 안전하게 데이터를 공유할 수 있도록 유지한다.

그러나 모든 데이터가 공유되지 않기 때문에 한 Task에서 생성된 데이터를 다른 Task에서 사용해야 할 경우 데이터를 직접 공유하면 문제가 발생할 수 있다.

즉 공유 데이터를 안전하게 사용하기 위해선 어떤 Task가 데이터에 접근할 때 다른 Task는 그 데이터에 접근하지 못하도록 해야 한다.

 

Actor isolation

Actor는 서로 다른 task에서 공유 데이터에 접근하는 방법을 data race를 발생시키지 않고 조정하는 방식으로 제공한다.

Actor는 concurrency 바다에서 섬에 해당한다.

보트와 같이 각 섬은 독립적(self-contained)이고, 각각 자체적인 상태(객체 내부에 저장된 데이터)를 가지고 있고 모든 다른 것들로부터 완전히 격리되어 있다.

 

각각의 상태에 접근하기 위해선, 코드가 섬 위에서 실행돼야 한다.

예를 들어, advanceTime 메서드는 이 섬에 격리되어 있다. 따라서 섬의 모든 상태에 접근할 수 있다.

섬에서 코드를 실행하기 위해선, 보트가 필요하다. 보트는 섬을 방문하여 섬 위에서 코드를 실행하고 그 상태에 접근할 수 있다.

 

실행 중인 코드에서 섬의 상태에 대한 동시적인 접근을 제한하기 위해 한번에 하나의 보트만 섬에 방문할 수 있다.

만약 다른 보트가 나타나면, 그들은 그들의 차례가 올 때까지 대기해야 한다 (await)

그리고 주어진 보트가 섬을 방문할 기회를 얻기까지 오랜 시간이 걸릴 수 있기 때문에 actor에 await 으로 표시된 잠재적인 정지 지점을 표시한다.

정지 지점에서 섬이 풀리면 다른 보트가 방문할 수 있다.

 

두 보트가 바다 위에서 만났던 것처럼, 보트와 섬 사이의 상호작용은 non-Sendable 타입이 둘 사이를 통과하지 않도록 하여 격리를 유지해야 한다.

예를 들어 보트에 있는 닭을 섬에 있는 닭 무리에 추가하려 하거나, 섬에서 닭을 입양하여 보트에 가져가려 하면 Sendable 검사를 통해 컴파일러는 두 격리 구역에서 객체를 공유하려는 시도를 차단한다.

Actor는 참조타입이나, class와 달리 프로퍼티와 코드를 격리하여 동시 접근을 방지한다.

따라서 다른 격리 구역으로부터 actor에 대한 참조를 갖는 것은 안전하다.

이것은 섬에 대한 지도가 있는 것과 같다. 지도를 사용하여 섬을 방문할 수 있지만, 접근하기 위해선 도킹 절차를 거쳐야 한다.

따라서 모든 actor타입은 암시적으로 Sendable이다.

 

Actor isolation은 속한 컨텍스트에 따라 결정된다.

 

💡 여기서 말하는 Actor isolation은 다른 actor와 데이터를 공유하지 않는 actor 자신의 내부 상태와 코드의 접근을 제어하는 것을 의미한다. actor가 격리된 상태에서만 해당 actor의 인스턴스 메서드나 클로저 등이 실행될 수 있으며, 이를 통해 다른 actor와의 동시 접근으로 인한 데이터 경합 문제를 방지할 수 있다.

 

인스턴스 프로퍼티와 actor 내부에 인스턴스 메서드나 extension은 각각 해당 actor에 고립되어 있다. - advanceTime() 메서드

reduce 알고리즘에 전달된 클로저와 같은 클로저들은 Sendable이 아니기 때문에 actor 내부에서 유지되어야 하며, 다른 actor에서 사용될 수 없다. - food.indices.reduce(0)…

Task initializer 또한 해당 컨텍스트에서 actor isolation을 상속받는다. 따라서 생성된 task는 초기화된 컨텍스트에서 실행되며, 해당 actor에 대한 액세스를 제공한다.

반면에, detached task는 생성된 컨텍스트와 완전히 독립되어 있으므로 해당 컨텍스트의 actor isolation을 상속받지 않는다.

여기서 detached task의 코드는 격리된 food 인스턴스를 참조하기 위해 await을 사용해야 하기 때문에 이것은 actor 외부에 있는 것으로 간주된다. 이것은 non-isolated 코드이다.

 

 

Non-isolated 코드는 어떤 actor에서도 실행되지 않는 코드이다.

actor 내부의 함수를 non-isolated 키워드를 사용하여 actor 외부로 옮길 수 있다.

그리고 detached task에 사용된 클로저와 같이 암묵적으로 non-isolated로 처리될 수도 있다.

따라서 actor에서 격리된 상태를 읽기 위해선 await 키워드를 사용하여 해당 섬에 방문하여 필요한 상태를 복사해야 한다.

Non-isolated async 코드는 항상 global cooperative pool(전역 큐와 같음)에서 실행된다.

이는 바다 위에 보트가 있을 때만 실행된다고 생각할 수 있다. 따라서 해당 섬을 떠나 작업을 수행해야 한다.

이때, Sendable이 아닌 데이터가 전달되지 않도록 확인해야 한다. 아래 코드에서는 Sendable이 아닌 Chiken 객체가 섬을 떠나려 하여 잠재적인 data race가 감지되었다.

 

위 greet 메서드는 non-isolated이고 동기 코드이다. 이것은 보트나 섬, 동시적인 개념과 연관이 없다.

greetOne 메서드는 actor-isolated이다. 이 동기 코드는 섬에서 호출되기 때문에 flock에 접근하여 Chicken을 처리하는데 자유롭다.

 

반면에, 만약 greet을 호출하는 non-isolated async 작업이 있다면, 이는 바다에 보트에서 실행된다.

대부분의 swift 코드는 동기적이고 non-isolated 하기 때문에 호출된 격리 구역에서만 작동하도록 설계되어 있다.

 

Actor isolation 요약

  • Actor은 프로그램의 나머지 부분과 격리된 상태를 유지한다.
  • 한 번에 하나의 task 만 actor에서 실행된다. 동시 접근 상태는 없다.
  • Sendable 체킹은 task가 actor에 들어가거나 나갈 때마다 적용되어 동기화되지 않은 mutable state가 빠져나가지 않도록 한다.
  • 이를 통해 actor를 Swift 동시 프로그램을 위한 설계 블록 중 하나로 만들어 준다.

 

actor 중에는 main actor라는 특별한 actor 또한 있다.

이것은 바다의 중간에 있는 큰 섬이라고 생각해 볼 수 있다.

이는 모든 그리기와 유저 상호작용등을 관리하는 메인 스레드에서 실행된다. 그래서 무언가를 그리기 원하면 main actor 섬에서 코드를 실행해야 한다.

이것은 UI에서도 굉장히 중요하다. “큰” 섬이라고 표현했듯이, 프로그램의 유저 인터페이스와 관련된 많은 상태(UI framework)들이 여기에 담겨 있다.

그러나 이것은 actor이기 때문에 역시 한 번에 하나의 작업만 실행할 수 있다. 따라서 main actor에서 너무 많거나, 오래 걸리는 작업을 추가하면 UI가 반응을 하지 않는 상황을 발생시킬 수 있다.

 

@MainActor 를 붙여 main actor isolation을 나타낸다.

이 속성은 함수나 클로저에 적용하여 코드가 main actor에서 실행되어야 함을 나타낸다. 그러면 이 코드는 main actor에 격리된다.

Swift 컴파일러는 main-actor-isolated 코드가 메인 스레드에서만 실행될 것을 보장한다. 다른 actor에 대한 동기화 메커니즘과 동일하다.

만약 non-isolated 컨텍스트에서 main actor로 메서드(updateView)를 호출하면, main actor 로 전환되는 것을 고려하여 await 을 추가해야 한다.

 

Main actor 속성은 타입에도 적용할 수 있다. 이 경우, 해당 타입의 인스턴스는 main actor에만 격리된다.

다시 말해, 프로퍼티는 main actor에서만 접근할 수 있으며, 메서드는 명시적으로 제외되지 않는 한 main actor로 분리된다.

일반적인 actor와 마찬가지로, main actor 클래스에 대한 참조는 데이터가 isolated 하기 때문에 Sendable이다.

이러한 이유로 메인 스레드에 의해 뷰와 뷰컨트롤러가 생성되는 UI에는 이 어노테이션이 적합하다.

프로그램 내 뷰 컨트롤러와 다른 task와 actor 간 참조를 공유할 수 있으며, 이러한 참조를 통해 뷰 컨트롤러에 비동기적으로 결과를 보낼 수 있다.

 

Main actor 속성은 타입에도 적용할 수 있다. 이 경우, 해당 타입의 인스턴스는 main actor에만 격리된다.

 

다시 말해, 프로퍼티는 main actor에서만 접근할 수 있으며, 메서드는 명시적으로 제외되지 않는 한 main actor로 분리된다.

일반적인 actor와 마찬가지로, main actor 클래스에 대한 참조는 데이터가 isolated 하기 때문에 Sendable 이다.

이러한 이유로 메인 스레드에 의해 뷰와 뷰컨트롤러가 생성되는 UI에는 이 어노테이션이 적합하다.

프로그램 내 뷰 컨트롤러와 다른 task와 actor 간 참조를 공유할 수 있으며, 이러한 참조를 통해 뷰 컨트롤러에 비동기적으로 결과를 보낼 수 있다.

 

Atomicity

Swift Concurrency model의 목표는 data race 를 제거하는 것이다. 이는 데이터 혼란과 관련된 저수준의 data race를 제거하는 것이다.

위에서 설명했듯이, actor는 한 번에 한 작업만 실행한다. 하지만, actor가 실행 중이 아닐 때, actor는 다른 작업을 실행할 수 있다. 이것은 프로그램에 단계를 추가해 deadlock에 대한 가능성을 방지한다.

그러나, 이는 await을 기준으로 actor의 불변성을 신중히 고려해야 한다는 것을 의미한다. 그렇지 않으면, 프로그램은 실제로 데이터가 손상되지 않았음에도 불구하고 예상치 못한 상태에 있을 수 있는 고수준의 data race 상태를 만들어 낼 수 있다.

 

여기 섬에서 파인애플을 추가로 예치하는 함수이다. actor 밖에 있기 때문에 이것은 non-isolated 코드이다.

즉 바다에서 실행된다. 그리고 파인애플들과 예치할 섬의 지도가 주어진다.

 

이 코드에서 가장 중요한 부분 중 하나는 섬으로부터 음식 배열의 복사본을 가져오는 부분이다.

이를 위해서 await 키워드로 섬을 방문해야 한다.

 

음식 배열을 복사하고 보트는 작업을 계속하기 위해 다시 바다로 돌아간다.

그 다음 한 개의 파인애플 파라미터를 가져와 섬에서 가져온 두 개의 파인애플에 추가하는 작업을 수행한다.

 

이제 보트는 다시 섬으로 복귀해 음식 배열을 세 개의 파인애플로 변경해 줄 것이다.

모든 것이 순조롭게 진행되었고 섬에 파인애플은 두 개에서 세 개로 변경되었다. 그러나 상황이 다르게 진행될 수 있다.

 

만약 해적선이 들이닥쳐서 섬의 파인애플을 모두 훔쳐갔다면, 이전의 보트가 섬을 방문할 때 응답을 받지 못할 것이다.

그런 다음 처음 보트는 달아와 세 개의 파인애플을 다시 섬에 가져다 놓는데 이때 문제를 알게 된다.

세 개의 파인애플은 해적선이 가져간 두 개의 파인애플을 포함해 다섯 개의 파인애플이 되었다!

이 경우, 같은 actor내부 인스턴스에 대한 두 개의 await 구문이 접근하고 있다.

await구문이기 때문에, 작업이 여기서 일시 중단될 수 있고, actor가 다른 높은 우선순위의 작업(예: 해적과의 전투)을 처리할 수 있다는 것을 의미한다.

이런 경우는 섬의 인스턴스가 두 await 사이에서 변경될 가능성이 있으며, 이로 인해 문제가 발생할 수 있다.

 

이러한 상황에서, swift 컴파일러는 액터에서 다른 액터의 상태를 직접 수정하려는 시도를 거부할 것이다.

그러나 위와 같이 actor 내에서 동기코드로 다시 작성해야 한다.

이것은 동기 코드이기 때문에 actor에서 중단 없이 실행된다. 따라서 함수 전체에서 섬의 인스턴스가 다른 누구에 의해 변경되지 않을 것임을 확신할 수 있다.

 

Think transactionally

 

💡 Transaction - 일괄적으로 처리되는 하나의 논리적 기능을 수행하기 위한 작업 단위, 즉 여러 개의 연산이 모여 하나의 기능을 수행하는 것 이러한 작업들은 모두 성공 또는 모두 실패해야 하며, 중간에 일부만 성공하는 경우 데이터 일관성에 문제가 발생할 수 있기 때문에, 원자성(Atomicity)의 특성을 가지고 있다.

 

  • Identify synchronous operations that can be interleaved
    • Actor를 작성할 때 동기적이고, 트랜잭션 형태의 작업을 고려해야 한다. 또한 서로 교차되어 실행할 수 있는 독립적인 작업이어야 한다.
  • Keep async actor operation simple
    • 비동기적인 actor 작업을 구성할 때, 동기적인 작업들을 기본으로 구성하고, await 구문이 있는 비동기 작업도 동기적인 작업을 기반으로 구성하여 일관된 상태를 유지하도록 구성해야 한다. 이를 통해 actor를 최대한 활용할 수 있으며, 낮은 수준의 data race와 높은 수준의 data race 모두 제거할 수 있다.

 

Ordering

Concurrent program에서 많은 일들이 한 번에 일어난다. 따라서 이러한 일이 발생하는 순서는 실행마다 다를 수 있다.

하지만 프로그램은 종종 사용자 입력 또는 서버로부터 오는 이벤트 스트림과 같은 일관된 순서로 이벤트를 처리하는 것에 의존한다.

이러한 이벤트 스트림이 들어오면, 순서대로 결과가 일어나길 기대한다.

 

Swift Concurrency는 작업을 순서대로 처리하기 위한 도구를 제공하지만, actor은 아니다.

Actor은 가장 높은 우선순위의 작업을 먼저 실행하여, 전체 시스템이 반응적으로 유지되도록 돕는다.

이것은 동일한 actor에서 더 높은 우선순위의 작업이 낮은 우선순위의 작업보다 먼저 실행되는 우선순위 역전을 제거한다. - 이는 serial Dispatch queue에서 선입선출 방식으로 실행되는 것과는 큰 차이가 있다.

 

Swift concurrency에는 작업을 정렬하기 위한 여러 가지 도구가 있다.

  • Task

Task는 보통의 제어 흐름을 따라 시작부터 끝까지 실행되므로, 자연스럽게 작업을 순서대로 처리할 수 있다.

AsyncStream은 실제 이벤트 스트림을 모델링하는 데 사용할 수 있다. 하나의 Task가 for-await-in 루프를 통해 스트림의 이벤트들을 반복하면서 각 이벤트를 차례대로 처리할 수 있다.

AsyncStream은 순서를 유지하면서 스트림에 이벤트를 추가할 수 있는 여러 이벤트 생성자와 공유할 수 있다.

위에서 Swift Concurrency 모델은 task와 actor의 경계에서 Sendable 체크를 통해 격리 개념을 사용하여 data race를 제거하도록 설계되었음을 알 수 있었다. 그러나, 우린 모든 Sendable 타입을 마크하기 위해서 현재 진행 중인 모든 작업을 중단할 순 없다.

 

Sendability

Swift 5.7에선 Sendability를 엄격하게 체크할 수 있는 빌드 설정이 도입되었다.

기본 설정은 Minimal로, 컴파일러는 Sendable로 표시한 경우에만 진단하기 때문에 이 경우엔 경고나 오류가 발생하지 않는다 - Swift 5.5, 5.6의 동작 방식과 유사하다

 

여기서 Sendable 일치를 추가하면 컴파일러가 Chicken이 Sendable타입이 아니기 때문에 Sendable이 될 수 없다는 오류를 알려준다.

하지만, 이 문제는 Swift 5에선 오류가 아닌 경고로 제공이 된다.

 

Data race 안전성을 더 향상시키기 위해선 Strict Concurrency Checking을 “Targeted”로 설정한다.

이 설정은 async/await, tasks, 또는 actors와 같은 Swift Concurrency 기능을 이미 채택한 코드에 대해 Sendable 체크를 활성화한다.

예를 들어, 새로 생성된 task 에서 non-Sendable타입의 값을 캡처하려는 시도를 식별할 수 있다.

 

몇몇 non-Sendable타입은 다른 모듈에서 온 경우도 있다. 즉, 패키지에 Sendable을 업데이트하지 않은 경우이거나, 자신의 모듈에서 아직 업데이트를 하지 않은 경우일 수 있다.

이 경우 @preconcurrency 속성을 사용하여 해당 모듈에서 온 타입에 대한 Sendable 경고를 일시적으로 비활성화할 수 있다. - 임시적으로 오류를 지워줄 필요가 있을 때

이렇게 하면 이 소스 파일에서 Chicken 타입에 대한 경고가 없어진다.

그리고 언젠가 FarmAnimals 모듈도 Sendable 적합성을 갖추게 될 것이다.

결국 Chicken이 Sendable이 되고 @preconcurrency 속성을 제거하거나, 그렇지 않으면 경고가 다시 나타날 것이다.

 

“Targeted” strictness 설정은 기존 코드와의 호환성과 잠재적인 경쟁을 식별하는 것 사이의 균형을 유지하는 것을 시도한다.

 

만약 데이터 경쟁이 발생할 수 있는 모든 위치를 확인하고 싶다면 “Complete” 옵션을 사용한다.

이는 데이터 경쟁을 완전히 제거하도록 의도된 Swift 6과 제일 유사하다.

다시 이전 두 모드에서 검사한 모든 것을 모든 코드에 대해 수행한다.

doWork 메서드에서는 concurrency 기능을 전혀 사용하지 않는다. 대신에 dispatch queue에서 작업을 수행하고 있으며, 이는 해당 코드를 동시에 실행한다.

Dispatch queue에서의 async 작업은 실제로 Sendable 클로져를 가져야 하므로, 컴파일러는 non-Sendable인 body()가 dispatch queue에서 실행되는 코드에 의해 캡처될 때 데이터 경쟁이 발생한다는 경고를 생성한다.

 

doWork메서드의 body 파라미터에 @Sendable 속성을 추가해 줌으로써 이 문제를 해결하고, 이제 doWork의 모든 호출자는 Sendable 클로저를 제공해야 함을 알 수 있게 된다.

이는 더 나은 데이터 경쟁 체킹을 할 수 있으며, 이제 visit메서드가 데이터 경쟁의 원인임을 알 수 있게 된다.

 

“Complete” 옵션은 프로그램에서 잠재적인 모든 데이터 경쟁을 드러내는 데 도움이 된다.

 

The road to data-race safety

  • Enables stricter concurrency checking on module at a time
    • Swift는 데이터 레이스를 제거하기 위한 목적을 가지고 있기 때문에, 우리는 결국 “Complete” 옵션을 통해 이 목표를 달성해야 한다.
    • 이를 통해 점진적으로 엄격한 동시성 검사를 통한 오류를 제거하는 방식으로 data race safety한 앱을 달성하도록 권장한다.
  • Use @preconcurrency to suppress warning from another module
    • 또한, 가져온 모듈의 타입에 대한 경고를 제거하기 위해 @preconcurrency 를 사용하는 것에 대한 걱정할 필요는 없다. 그 모듈이 엄격한 동시성 검사를 채택하면, 컴파일러는 새로운 검사를 수행할 수 있다.

 

이 길의 끝에서, 우린 메모리 안전성과 데이터 경합 안전성을 모두 갖게 되어 더 나은 앱을 구축하는데 집중할 수 있게 되었다.

지금까지 sea of concurrency 였습니다.