스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
구조화된 동시성의 기초를 넘어
핵심은 작업 트리에 있습니다. 앱이 자동 작업 취소, 작업 우선순위 전파, 유용한 작업 로컬 값 패턴을 관리하는 데 구조화된 동시성이 어떻게 도움이 되는지 알아보세요. 유용한 패턴과 최신 작업 그룹 API로 앱의 리소스를 관리하는 법을 배우세요. 작업 트리와 작업 로컬 값의 힘을 활용하여 분산 시스템을 통찰할 수 있는 방법을 알아봅니다. 시청하시기 전에 WWDC21의 'Swift 동시성의 이면'과 'Swift의 구조화된 동시성 살펴보기' 세션을 보시고 Swift 동시성과 구조화된 동시성의 기초를 복습하시기 바랍니다.
챕터
- 0:56 - Structured concurrency
- 3:11 - Task tree
- 3:44 - Task cancellation
- 5:26 - withTaskCancellationHandler
- 8:36 - Task priority
- 10:23 - Patterns with task groups
- 11:27 - Limiting concurrent tasks in TaskGroups
- 12:22 - DiscardingTaskGroup
- 13:53 - Task-local values
- 16:58 - swift-log
- 17:19 - MetadataProvider
- 18:58 - Task traces
- 19:46 - Swift-Distributed-Tracing
- 20:42 - Instrumenting distributed computations
- 23:38 - Wrap-up
리소스
관련 비디오
WWDC23
WWDC22
WWDC21
-
다운로드
♪ ♪
안녕하세요 에번이라고 합니다 오늘은 구조화된 동시성의 기본에서 더 나아가 구조화된 작업이 어떻게 유용한 동작 실현을 단순화할 수 있는지 보겠습니다 들어가기에 앞서 구조화된 동시성을 처음 접하거나 복습하고 싶다면 WWDC21의 'Swift의 구조화된 동시성 살펴보기' 세션과 'Swift 동시성의 이면' 세션을 보셔도 좋습니다 오늘은 작업 계층 구조를 살펴보고 이것이 어떻게 자동 작업 취소, 우선순위 전파 및 유용한 작업 로컬 값 동작을 가능하게 하는지 보겠습니다 그다음 작업 그룹을 이용한 패턴을 다루어 리소스 사용 관리에 도움이 되게 하겠습니다 마지막으로는 이 모든 것을 어떻게 결합하여 서버 환경에서 프로파일링과 작업 추적을 용이하게 할 수 있는지 보겠습니다 구조화된 동시성을 사용하면 동시적 코드를 추론할 수 있는데 실행이 분기하여 동시에 실행되고 해당 실행 결과가 재합류하는 지점을 잘 정의하여 그 지점을 사용하면 됩니다 if 블록과 for 루프가 동기식 코드에서 제어 흐름의 동작을 정의하는 방식과 비슷하죠 동시적 실행을 트리거하려면 async let이나 작업 그룹을 사용하거나 작업 또는 분리된 작업을 만들면 됩니다 결과는 중단 지점에서 현재의 실행과 재합류하는데 이는 await로 나타납니다 모든 작업이 구조화된 건 아닙니다 구조화된 작업은 async let과 작업 그룹으로 만들어지지만 비구조화된 작업은 Task와 Task.detached로 만들어지죠 구조화된 작업은 작업이 선언된 스코프에서 끝까지 살아남습니다 로컬 변수처럼요 그리고 스코프 밖으로 나가면 자동으로 취소되므로 작업의 수명을 분명히 알 수 있습니다 가능한 한 구조화된 작업을 사용하세요 나중에 논의할 구조화된 동시성의 장점은 비구조화된 작업에는 해당하지 않을 수 있기 때문입니다 코드를 파헤치기 전에 구체적인 예제를 찾아봅시다 부엌에서 요리사 여럿이 수프를 만들고 있다고 칩시다 수프를 조리하려면 여러 단계를 거쳐야 합니다 요리사들이 재료를 썰고 닭고기를 재우고 국물을 끓인 다음 조리해야 비로소 수프를 낼 수 있죠 어떤 작업은 동시에 진행할 수 있지만 어떤 작업은 특정한 순서에 따라 진행해야만 합니다 이걸 코드로 어떻게 표현할 수 있는지 봅시다 일단 makeSoup 함수에 집중하겠습니다 여러분은 어쩌면 구조화되지 않은 작업을 만들어 함수에 동시성을 부여하고 필요시 작업의 값을 기다리실지도 모르겠군요 이러면 어느 작업이 동시에 실행할 수 있는 작업인지 표현되기는 하지만 이는 Swift에서 권장하는 동시성 이용법은 아닙니다 구조화된 동시성을 이용해 같은 함수를 표현해 봤습니다 만들어야 할 자식 작업의 개수를 알고 있으므로 편리한 async let문을 사용하면 됩니다 그러면 이 작업들은 부모 작업과 구조화된 관계를 형성하는데요 이게 왜 중요한지는 곧 설명하겠습니다 makeSoup는 비동기식 함수를 여러 개 호출하는데 그중 하나인 chopIngredients는 재료의 목록을 가져와 작업 그룹을 사용해 모든 재료를 동시에 썹니다 이제 makeSoup에 익숙해졌으니 작업 계층 구조를 봅시다 색이 있는 상자는 자식 작업을 나타내고 화살은 부모 작업에서 자식 작업 방향을 가리킵니다 makeSoup에는 자식 작업이 셋인데 재료 썰기 닭고기 재우기, 국물 끓이기입니다 chopIngredients는 작업 그룹을 사용해 각 재료에 해당하는 자식 작업을 만듭니다 재료가 세 가지라면 자식도 세 개 만들죠 부모 자식 계층 구조에서 작업 트리가 만들어집니다 이제 작업 트리를 도입했으니 이게 우리 코드에 어떻게 도움이 될지 알아봅시다 작업 취소는 앱에 작업 결과가 더는 필요하지 않으므로 작업을 중지하고 부분적인 결과만을 반환하거나 오류를 발생시켜야 한다는 신호를 보낼 때 쓰입니다 수프 예제의 경우 손님이 떠나거나 주문을 바꾸거나 폐점 시간이 됐다면 수프 만들기를 중단하겠죠 작업 취소를 일으키는 원인은 무엇일까요? 구조화된 작업은 스코프를 벗어나면 암묵적으로 취소됩니다 작업 그룹에 cancelAll을 호출해 활성화된 자식 작업과 앞으로 생길 자식 작업을 모두 취소할 수도 있지만요 비구조화된 작업은 cancel 함수를 이용해 명시적으로 취소됩니다 부모 작업을 취소하면 자식 작업도 모두 취소됩니다 취소는 협동적이므로 자식 작업이 곧바로 중지되지는 않습니다 단지 해당 작업에 isCancelled 플래그가 생길 뿐이죠 실제 취소는 사실 코드 안에서 이뤄집니다 취소는 경쟁이라서 검사 전에 작업을 취소하면 makeSoup는 SoupCancellationError를 발생시키고 가드가 실행된 후에 작업을 취소하면 프로그램은 계속해서 수프를 조리합니다 부분적인 결과를 반환하는 대신 취소 오류를 발생시키려면 Task.checkCancellation을 호출하면 됩니다 그러면 작업이 취소됐을 때 이게 CancellationError를 발생시키죠 비싼 작업을 시작하기 전에 작업 취소 상태를 검사하는 것은 중요합니다 그래야 결과가 지금도 필요한지 검증할 수 있으니까요 취소 검사는 동기식이므로 취소에 반응해야 하는 모든 함수는 동기식이든 비동기식이든 진행 전에 작업 취소 상태를 검사해야 합니다 isCancelled나 checkCancellation으로 폴링하는 것은 작업이 실행 중일 때는 유용하지만 때로는 작업이 일시 중단되어 실행되는 코드가 없을 때 취소에 반응해야 하는 상황도 있습니다 AsyncSequence를 구현할 때처럼요 withTaskCancellationHandler는 이런 경우에 유용합니다 시프트 함수를 도입해 봅시다 요리사는 주문이 들어오는 대로 수프를 만들겠죠 작업 취소 신호를 받아 교대 근무 시간이 끝날 때까지요 한 취소 시나리오에서는 비동기적인 for 루프가 루프 취소 전에 새 주문을 받으면 makeSoup 함수는 아까 우리가 정의한 대로 취소를 처리하여 오류를 발생시킵니다 다른 시나리오에서는 다음 주문을 기다리며 작업이 일시 중단됐을 때 취소가 발생합니다 이 사례는 좀 더 흥미롭습니다 작업이 실행되고 있지 않아서 취소 이벤트를 명시적으로 폴링할 수 없으니까요 그 대신 취소 핸들러를 사용해 취소 이벤트를 탐지해서 비동기적 for 루프에서 빠져나와야 합니다 주문은 AsyncSequence에서 생성됩니다 AsyncSequence는 AsyncIterator로 구동되는데 AsyncIterator는 비동기적인 next 함수죠 동기적 반복자와 마찬가지로 next 함수도 시퀀스의 다음 엘리먼트를 반환하거나 nil을 반환하여 시퀀스 끝에 도달했음을 나타냅니다 AsyncSequence는 많은 경우 상태 기계로 구현되는데 상태 기계는 시퀀스 실행을 멈추는 데 쓰입니다 이 예제에서는 isRunning이 true일 때 시퀀스는 계속해서 주문을 발생시켜야 합니다 작업이 취소되면 시퀀스가 끝났으니 정지해야 한다고 알려야 하죠 그러려면 시퀀스 상태 기계에서 cancel 함수를 동기적으로 호출하면 됩니다 취소 핸들러가 즉각 실행되므로 상태 머신은 동시적으로 실행할 수 있는 취소 핸들러와 본문 사이의 변경 가능한 공유 상태입니다 상태 기계는 보호해야 합니다 액터는 캡슐화된 상태를 보호하는 데는 좋지만 우리는 상태 기계에서 개별 프로퍼티를 수정하고 읽고자 하므로 액터는 그다지 적합한 도구가 아닙니다 게다가 액터에 실행되는 연산의 순서를 보장할 수 없으므로 취소가 먼저 실행될지도 확실히 알 수 없습니다 다른 무언가가 필요합니다 저는 Swift Atomics 패키지에 있는 아토믹을 쓰기로 했지만 디스패치 큐나 록을 써도 됩니다 이런 메커니즘을 사용하면 공유 상태를 동기화하고 경쟁 상태를 피하면서도 실행 중인 상태 기계를 취소할 수 있죠 취소 핸들러에 비구조화된 작업을 도입하지 않아도요 작업 트리는 작업 취소 정보를 자동으로 전파합니다 우리는 취소 토큰과 동기화를 신경 쓸 필요 없이 Swift 런타임이 알아서 안전하게 처리하도록 두면 됩니다 기억하세요, 취소해도 작업 실행이 멈추지는 않습니다 단지 작업이 취소됐으니 최대한 빨리 작업을 끝내라는 신호를 작업에 보낼 뿐이죠 취소 여부를 검사하는 건 코드의 몫입니다 다음으로는 구조화된 작업 트리가 어떻게 우선순위를 전파하고 우선순위 역전을 피하는 데 도움이 되는지 살펴보겠습니다 첫째, 우선순위란 무엇이고 이게 왜 중요한가요? 우선순위는 해당 작업이 얼마나 긴급한지를 시스템에 알리는 방법입니다 버튼을 누를 때 반응하는 것 같은 특정한 작업은 즉각 실행하지 않으면 앱이 멈춘 것처럼 보이게 됩니다 반면 서버에서 콘텐츠를 프리페칭하는 것 같은 작업은 누구의 눈에도 띄지 않게 백그라운드에서 실행할 수 있죠 둘째, 우선순위 역전이란 무엇일까요? 우선순위 역전은 우선순위가 높은 작업이 우선순위가 낮은 작업의 결과를 기다릴 때 발생합니다 기본적으로 자식 작업은 부모에게 우선순위를 상속받으므로 makeSoup가 실행하는 작업의 우선순위가 중간이라고 가정한다면 실행되는 자식 작업의 우선순위도 모두 중간이겠죠 예컨대 식당의 VIP 고객이 수프를 찾는다고 하면 VIP의 수프를 더 우선으로 하겠죠 좋은 평가를 받기 위해서요 VIP가 수프를 기다리는 동안 모든 자식 작업의 우선순위도 높아집니다 그래야 우선순위가 높은 작업이 우선순위가 낮은 작업 때문에 미뤄지지 않아 우선순위 역전을 피하게 되니까요 우선순위가 더 높은 작업의 결과를 기다리면 작업 트리의 자식 작업 모두 우선순위가 상승합니다 주의할 점은 작업 그룹의 다음 결과를 기다리면 그룹 내 모든 자식 작업의 우선순위가 상승한다는 겁니다 어떤 작업이 다음으로 완료될 가능성이 가장 높은지 모르니까요 동시성 런타임은 우선순위 큐로 작업 스케줄을 잡으므로 우선순위가 높은 작업은 우선순위가 낮은 작업보다 먼저 선택되어 실행됩니다 작업의 우선순위가 높아지면 남은 수명 내내 높게 유지되며 우선순위 상승을 되돌릴 방법은 없습니다 수프를 빨리 내서 VIP 고객을 효과적으로 만족시키고 좋은 평가를 받은 덕에 주방의 인기가 높아지고 있습니다 우리는 자원을 반드시 효과적으로 사용하고 싶은데 살펴보니 우리가 재료 썰기 작업을 많이 만들고 있네요 작업 그룹으로 동시성을 관리하는 유용한 패턴 몇 가지를 알아봅시다 도마를 놓을 수 있는 자리는 한정되어 있습니다 너무 많은 재료를 동시에 썬다면 다른 작업을 할 공간이 모자라니 동시에 써는 재료의 개수를 제한해야겠습니다 다시 코드로 돌아가서 썰기 작업을 만드는 루프를 조사해야겠습니다 각 재료에 대한 원본 루프를 썰기 작업의 최대 개수로 시작하는 루프로 대체합니다 그다음, 이전 작업이 끝날 때마다 루프가 결과를 수집하여 새 작업을 시작하도록 합니다 새 루프는 실행 중인 작업 하나가 끝날 때까지 기다렸다가 썰어야 할 재료가 아직 남아 있는 동안 다음 재료를 썰라는 작업을 새로 추가합니다 이 아이디어를 좀 더 정제하여 패턴을 더 명확히 살펴보겠습니다 최초의 루프는 동시적 작업이 너무 많이 생성되지 않도록 최대 개수만큼만 작업을 생성합니다 최대 개수만큼 작업이 실행되면 그중 하나가 끝나기를 기다립니다 작업 하나가 끝났는데 중지 조건을 충족하지 않는다면 새로운 작업을 생성하여 계속 진도를 나갑니다 이러면 그룹에 있는 동시적 작업의 개수가 제한됩니다 먼저 시작한 작업이 끝나지 않으면 새 작업을 시작하지 않으니까요 앞에서 요리사들이 교대 근무를 하고 취소를 이용해 교대 근무 시간이 끝나는 걸 알린다고 했죠 '주방 서비스' 코드는 다음과 같이 교대 근무를 처리합니다 요리사들은 각자의 작업을 하며 교대 근무를 시작합니다 요리사들이 일할 때 우리는 타이머를 맞추고 타이머가 다 돌아가면 진행 중인 근무를 모두 취소합니다 보시다시피 어떤 작업도 값을 반환하지 않습니다 Swift 5.9에 withDiscardingTaskGroup API가 새로 생겼는데요 폐기 작업 그룹은 완료된 자식 작업의 결과를 유지하지 않습니다 작업에서 사용한 리소스는 작업이 끝나자마자 자유롭게 쓸 수 있습니다 실행 메서드를 바꿔 폐기 작업 그룹을 활용할 수 있습니다 폐기 작업 그룹은 자기 자식을 자동으로 정리하므로 명시적으로 그룹을 취소하고 정리할 필요가 없습니다 폐기 작업 그룹에는 형제 자동 취소 기능도 있어서 자식 작업 중 하나가 오류를 발생시키면 나머지 작업도 모두 자동으로 취소됩니다 그래서 우리의 사용 예에 가장 적합하죠 근무 시간이 끝날 때 TimeToCloseError를 발생시키면 모든 요리사의 근무 시간이 자동으로 끝납니다 새로운 폐기 작업 그룹은 작업 하나가 끝날 때마다 리소스를 방출합니다 여러분이 결과를 수집해야 하는 일반 작업 그룹과는 다르죠 그 덕에 아무것도 반환할 필요 없는 작업이 많을 때 메모리 소비를 줄일 수 있습니다 예를 들면 계속 이어지는 요청을 처리할 때요 작업 그룹이 값을 반환하게 하면서도 동시적 작업의 개수를 제한하고 싶을 때가 있으실 겁니다 앞서 다룬 일반적 패턴을 이용하면 한 작업이 끝나야 다른 작업이 시작하므로 작업이 폭발적으로 늘지 않습니다 우리는 그 어느 때보다도 효율적으로 수프를 만들고 있지만 수프를 더 많이 만들 필요가 있습니다 이제 생산을 서버로 이전할 때입니다 그러면 주문을 처리하면서 추적해야 하는 어려움이 생기는데 그럴 때 작업 로컬 값이 도움이 됩니다 작업 로컬 값이란 주어진 작업, 더 정확히 말하면 작업 계층 구조와 연결된 데이터입니다 작업 로컬 값은 전역 변수와 비슷하지만 작업 로컬 값에 바인딩된 값은 현재의 작업 계층 구조에서만 나올 수 있습니다 작업 로컬 값은 정적 프로퍼티로 선언되는데 이때 TaskLocal 프로퍼티 래퍼를 씁니다 작업 로컬은 옵셔널로 설정하는 게 좋습니다 값 집합이 없는 작업은 기본값을 반환해야 하는데 기본값은 nil 옵셔널로 손쉽게 나타낼 수 있습니다 바인딩되지 않은 작업 로컬은 자기의 기본값을 담고 있습니다 이 사례에는 옵셔널인 String이 있으므로 이것은 nil이고 현재의 작업에 연결된 요리사는 없습니다 작업 로컬 값은 명시적으로 지정될 수 없고 특정한 스코프에 바인딩되어야 합니다 바인딩은 스코프가 지속되는 동안 유지되며 스코프가 끝나면 원래 값으로 되돌아갑니다 다시 작업 트리로 돌아가서 보면 각각의 작업에는 작업 로컬 값에 연결된 자리가 있습니다 수프를 만들기 전에 Sakura라는 이름을 cook 작업 로컬 변수에 바인딩했습니다 바인딩된 값을 저장하는 건 makeSoup뿐이며 자식들은 작업 로컬 스토리지에 어떤 값도 저장하지 않습니다 작업 로컬 변수에 바인딩된 값을 찾으려면 그 값을 가진 작업을 찾을 때까지 부모 각각을 반복해서 탐색해야 합니다 바인딩된 값을 가진 작업을 찾으면 작업 로컬은 그 값을 취합니다 부모 작업이 없는 뿌리에 도달하면 작업 로컬은 바인딩되지 않았으며 우린 원래의 기본값을 찾게 됩니다 Swift 런타임은 이런 쿼리를 더 빨리 실행하도록 최적화됐습니다 트리를 탐색하는 대신 우리가 찾는 키가 있는 작업을 직접 참조하면 되죠 작업 트리의 반복적인 성질은 이전 값을 잃지 않으면서 값을 섀도잉하는 데 유용합니다 지금 우리가 수프 제조 과정의 어느 단계에 있는지 추적하려면 makeSoup에 있는 soup에 step 변수를 바인딩한 다음 chopIngredients에 있는 chop에 다시 바인딩하면 됩니다 chopIngredients에 바인딩된 값은 이전 값을 섀도잉할 겁니다 원래 값이 관찰되는 chopIngredients에서 우리가 돌아올 때까지요 영상 편집이라는 마법의 힘을 빌린 덕에 우리는 서비스를 클라우드로 이전하여 수프에 대한 수요를 감당할 수 있게 됐습니다 수프 만드는 기능은 그대로이지만 이제는 기능이 서버에 가 있죠 주문이 시스템을 통과할 때 주문을 관찰할 수 있어야 주문이 제때 완료되는 걸 확인하고 예상치 못한 실패가 발생할지 모니터링할 수 있습니다 서버 환경은 여러 주문을 동시에 처리하므로 특정 주문을 추적할 수 있게 하는 정보를 포함해야 합니다 수동으로 로깅하는 작업은 반복적이고 장황하므로 소소한 버그와 오타가 생기기 쉽습니다 이런, 주문 ID만 로깅해야 하는데 실수로 주문 전체를 로깅했군요 작업 로컬 값을 활용해 더욱 정확하게 로깅하는 법을 알아봅시다 Apple 기기에서는 OSLog API를 계속 직접 사용하고 싶으시겠지만 응용 프로그램 일부가 클라우드로 옮겨 갔으니 다른 해결책이 필요하실 겁니다 SwiftLog는 로깅 API 패키지로 다양한 지원 구현을 갖추었으며 이걸 사용하면 서버를 변경하지 않고도 필요에 맞는 로깅 백엔드를 드롭인할 수 있습니다 MetadataProvider는 SwiftLog 1.5에 생긴 새 API입니다 메타데이터 공급자를 구현하면 로깅 로직을 쉽게 추상화하여 관련 값에 대한 정보를 확실히 일관되게 내보낼 수 있습니다 메타데이터 공급자는 딕셔너리와 비슷한 구조를 사용합니다 로깅되는 값에 이름을 매핑하는 거죠 우리는 orderID 작업 로컬 변수를 자동으로 로깅하고 싶으니 변수가 정의됐는지를 확인하고 정의됐다면 딕셔너리에 추가합니다 다수의 라이브러리가 라이브러리별 정보를 찾기 위해 자기만의 메타데이터 공급자를 정의할 수도 있습니다 그래서 MetadataProvider는 '멀티플렉스' 함수를 정의하는데 이 함수는 여러 메타데이터 공급자를 결합해 하나의 객체로 만듭니다 메타데이터 공급자 하나가 생겨서 그 공급자로 로깅 시스템을 초기화하면 로깅할 준비는 끝난 겁니다 로그에는 메타데이터 공급자에 명시된 정보가 자동으로 포함되므로 로그 메시지에 정보를 포함하는 걸 신경 쓰지 않아도 됩니다 로그는 주문 0이 주방에 들어올 때 요리사가 어디에서 주문을 받는지 보여줍니다 메타데이터 공급자의 값은 로그에 명확히 나열되므로 수프 조리 과정에서 주문을 추적하기가 더 쉬워집니다 작업 로컬 값을 사용하면 작업 계층 구조에 정보를 첨부할 수 있습니다 분리된 작업을 제외한 모든 작업은 현재의 작업에서 작업 로컬 값을 상속받습니다 작업들은 주어진 스코프 안에서 특정한 작업 트리에 바인딩되어 저수준 빌딩 블록을 제공하는데 이것을 사용해서 작업 계층 구조를 통해 추가 콘텍스트 정보를 전파할 수 있습니다 이제 작업 계층 구조와 이것이 제공하는 도구로 동시적 분산 시스템을 추적하고 프로파일링하겠습니다 Apple 플랫폼에서 동시성을 다룰 때 Instruments는 아군이 되어 줍니다 Swift 동시성 인스트루먼트는 구조화된 작업 간의 관계를 파악할 수 있게 하죠 더 자세히 알고 싶다면 이 세션을 참고하세요 제목은 'Swift 동시성을 시각화 및 최적화하기'입니다 또한 'Instruments의 HTTP 트래픽 분석하기' 세션에서 Instruments는 HTTP 트래픽 인스트루먼트를 소개했습니다 HTTP 트래픽 분석기는 로컬에서 발생하는 이벤트 추적만을 보여줍니다 서버 응답을 기다리는 동안 프로파일은 회색 상자를 띄우므로 서버의 성능을 어떻게 향상할 수 있을지 이해하려면 정보가 더 필요합니다 새로 나온 Swift 분산 추적 패키지를 소개합니다 작업 트리는 단일 작업 계층 구조에서 자식 작업을 관리하기에 좋습니다 분산 추적을 이용하면 여러 시스템에서 작업 트리의 이점을 활용하여 성능 특성과 작업 관계를 파악할 수 있습니다 Swift 분산 추적 패키지는 OpenTelemetry 프로토콜 구현을 포함하므로 Zipkin이나 Jaeger 같은 기존의 추적 솔루션도 훌륭하게 작동합니다 Swift 분산 추적의 목표는 Xcode Instruments의 불투명한 '응답 대기 중'에 서버에 일어나는 일에 대한 자세한 정보를 채워 넣는 것입니다 서버 코드를 계측해서 어디에 집중해야 할지 알아내야겠군요 분산 추적은 로컬에서 프로세스를 추적하는 것과는 좀 다릅니다 함수 하나당 추적을 하나씩 하는 대신 withSpan API를 사용해 스팬으로 코드를 계측합니다 스팬을 사용하면 추적 시스템이 보고한 코드 영역에 이름을 지정할 수 있습니다 스팬은 함수 전체를 커버할 필요는 없습니다 스팬은 주어진 함수의 특정 부분에 대해 더 많은 통찰을 제공할 수 있습니다 withSpan은 추가 추적 ID와 기타 메타데이터로 우리 작업에 주석을 달아서 추적 시스템이 작업 트리를 단일한 추적으로 병합할 수 있게 합니다 추적 시스템에는 작업 구조에 대한 통찰을 줄 수 있는 정보가 충분할 뿐만 아니라 작업의 런타임 성능 특성에 대한 정보도 있습니다 스팬의 이름은 추적 UI에 표시됩니다 이름은 짧고 대상을 묘사하는 게 좋습니다 그래야 특정 스팬의 정보를 혼잡 없이 쉽게 찾을 수 있죠 스팬 속성을 사용하면 추가 메타데이터를 쉽게 첨부할 수 있으므로 스팬 이름을 주문 ID로 혼잡하게 만들지 않아도 됩니다 여기서는 스팬 이름 대신 #function 지시어를 넣어서 스팬 이름에 함수 이름이 자동으로 채워지게 했고 스팬 속성을 사용해 현재의 주문 ID를 추적기에 보고된 스팬 정보에 첨부했습니다 대체로 추적 시스템은 특정 스팬을 검사하는 동안 속성을 표시합니다 대부분의 스팬에는 HTTP 상태 코드와 요청 및 응답의 크기 시작 시간과 종료 시간 및 다른 메타데이터가 있어서 시스템을 통과하는 정보를 추적하기가 더 쉽습니다 이전 슬라이드에서 언급했듯이 여러분 고유의 속성을 정의할 수도 있습니다 스팬을 활용하는 사례를 더 많이 보고 싶으시다면 swift-distributed-tracing-extras 리포지토리를 보세요 작업이 실패해서 오류를 발생시키면 그 정보는 스팬에도 표시되고 추적 시스템에도 보고됩니다 스팬에는 타이밍 정보와 트리에 있는 작업 간 관계가 들어 있으므로 스팬은 타이밍 경쟁이 초래한 오류를 추적하고 오류가 다른 작업에 어떤 영향을 미치는지 파악하는 데 유용합니다 지금까지 추적 시스템과 추적 시스템이 추적 ID로 작업 트리를 재구성하는 방법과 스팬에 고유한 속성을 첨부하는 방법을 설명했지만 아직 이걸 분산 시스템에 적용하지는 않았습니다 추적 시스템의 멋진 점은 더 이상 해야 할 일이 없다는 것입니다 만약에 주방 서비스에서 썰기 서비스를 팩터링하는데 그 외에는 동일한 코드를 유지한다면 추적 시스템은 자동으로 흔적을 추적하여 분산 시스템의 여러 기계에 전달할 것입니다 추적 뷰에는 스팬들이 다른 기계에서 실행 중이라고 나오겠지만 그 외의 다른 것들은 변하지 않을 겁니다 분산 추적은 시스템의 모든 부분이 추적을 받아들일 때 가장 강력한 힘을 발휘하는데 여기에는 HTTP 클라이언트와 서버, 기타 RPC 시스템도 포함됩니다 Swift 분산 추적은 작업 트리를 토대로 빌드한 작업 로컬 값을 활용하여 신뢰성 있는 노드 간 추적을 생성하는 데 필요한 모든 정보를 자동으로 전파합니다 구조화된 작업은 동시적 시스템의 비밀을 해제하여 여러분에게 연산을 자동으로 취소할 수 있는 도구와 우선순위 정보를 자동으로 전파하는 도구 복잡한 분산 워크로드를 쉽게 추적하는 도구를 제공합니다 이 모든 것은 Swift의 동시성이 구조화되어 있기에 가능합니다 이 세션을 통해 여러분이 구조화된 동시성에 관심이 생기고 앞으로 비구조화된 방법 대신 구조화된 작업을 활용하시기를 바랍니다 시청해 주셔서 감사합니다 여러분이 구조화된 동시성으로 어떤 유용한 패턴을 만들어 내실지 궁금하네요 수프가 맛있겠군요 ♪ ♪
-
-
2:27 - Unstructured concurrency
func makeSoup(order: Order) async throws -> Soup { let boilingPot = Task { try await stove.boilBroth() } let choppedIngredients = Task { try await chopIngredients(order.ingredients) } let meat = Task { await marinate(meat: .chicken) } let soup = await Soup(meat: meat.value, ingredients: choppedIngredients.value) return await stove.cook(pot: boilingPot.value, soup: soup, duration: .minutes(10)) }
-
2:42 - Structured concurrency
func makeSoup(order: Order) async throws -> Soup { async let pot = stove.boilBroth() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) }
-
3:00 - Structured concurrency
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients for ingredient in ingredients { group.addTask { await chop(ingredient) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for await choppedIngredient in group { if choppedIngredient != nil { choppedIngredients.append(choppedIngredient!) } } return choppedIngredients } }
-
4:32 - Task cancellation
func makeSoup(order: Order) async throws -> Soup { async let pot = stove.boilBroth() guard !Task.isCancelled else { throw SoupCancellationError() } async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) }
-
4:58 - Task cancellation
func chopIngredients(_ ingredients: [any Ingredient]) async throws -> [any ChoppedIngredient] { return try await withThrowingTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in try Task.checkCancellation() // Concurrently chop ingredients for ingredient in ingredients { group.addTask { await chop(ingredient) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for try await choppedIngredient in group { if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
5:47 - Cancellation and async sequences
actor Cook { func handleShift<Orders>(orders: Orders) async throws where Orders: AsyncSequence, Orders.Element == Order { for try await order in orders { let soup = try await makeSoup(order) // ... } } }
-
6:41 - Cancellation and async sequences
public func next() async -> Order? { return await withTaskCancellationHandler { let result = await kitchen.generateOrder() guard state.isRunning else { return nil } return result } onCancel: { state.cancel() } }
-
7:40 - AsyncSequence state machine
private final class OrderState: Sendable { let protectedIsRunning = ManagedAtomic<Bool>(true) var isRunning: Bool { get { protectedIsRunning.load(ordering: .acquiring) } set { protectedIsRunning.store(newValue, ordering: .relaxed) } } func cancel() { isRunning = false } }
-
10:55 - Limiting concurrency with TaskGroups
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients for ingredient in ingredients { group.addTask { await chop(ingredient) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for await choppedIngredient in group { if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
11:01 - Limiting concurrency with TaskGroups
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients let maxChopTasks = min(3, ingredients.count) for ingredientIndex in 0..<maxChopTasks { group.addTask { await chop(ingredients[ingredientIndex]) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for await choppedIngredient in group { if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
11:17 - Limiting concurrency with TaskGroups
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients let maxChopTasks = min(3, ingredients.count) for ingredientIndex in 0..<maxChopTasks { group.addTask { await chop(ingredients[ingredientIndex]) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] var nextIngredientIndex = maxChopTasks for await choppedIngredient in group { if nextIngredientIndex < ingredients.count { group.addTask { await chop(ingredients[nextIngredientIndex]) } nextIngredientIndex += 1 } if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
11:26 - Limiting concurrency with TaskGroups
withTaskGroup(of: Something.self) { group in for _ in 0..<maxConcurrentTasks { group.addTask { } } while let <partial result> = await group.next() { if !shouldStop { group.addTask { } } } }
-
11:56 - Kitchen Service
func run() async throws { try await withThrowingTaskGroup(of: Void.self) { group in for cook in staff.keys { group.addTask { try await cook.handleShift() } } group.addTask { // keep the restaurant going until closing time try await Task.sleep(for: shiftDuration) } try await group.next() // cancel all ongoing shifts group.cancelAll() } }
-
12:41 - Introducing DiscardingTaskGroups
func run() async throws { try await withThrowingDiscardingTaskGroup { group in for cook in staff.keys { group.addTask { try await cook.handleShift() } } group.addTask { // keep the restaurant going until closing time try await Task.sleep(for: shiftDuration) throw TimeToCloseError() } } }
-
14:10 - TaskLocal values
actor Kitchen { @TaskLocal static var orderID: Int? @TaskLocal static var cook: String? func logStatus() { print("Current cook: \(Kitchen.cook ?? "none")") } } let kitchen = Kitchen() await kitchen.logStatus() await Kitchen.$cook.withValue("Sakura") { await kitchen.logStatus() } await kitchen.logStatus()
-
16:17 - Logging
func makeSoup(order: Order) async throws -> Soup { log.debug("Preparing dinner", [ "cook": "\(self.name)", "order-id": "\(order.id)", "vegetable": "\(vegetable)", ]) // ... } func chopVegetables(order: Order) async throws -> [Vegetable] { log.debug("Chopping ingredients", [ "cook": "\(self.name)", "order-id": "\(order.id)", "vegetable": "\(vegetable)", ]) async let choppedCarrot = try chop(.carrot) async let choppedPotato = try chop(.potato) return try await [choppedCarrot, choppedPotato] } func chop(_ vegetable: Vegetable, order: Order) async throws -> Vegetable { log.debug("Chopping vegetable", [ "cook": "\(self.name)", "order-id": "\(order)", "vegetable": "\(vegetable)", ]) // ... }
-
17:33 - MetadataProvider in action
let orderMetadataProvider = Logger.MetadataProvider { var metadata: Logger.Metadata = [:] if let orderID = Kitchen.orderID { metadata["orderID"] = "\(orderID)" } return metadata }
-
17:50 - MetadataProvider in action
let orderMetadataProvider = Logger.MetadataProvider { var metadata: Logger.Metadata = [:] if let orderID = Kitchen.orderID { metadata["orderID"] = "\(orderID)" } return metadata } let chefMetadataProvider = Logger.MetadataProvider { var metadata: Logger.Metadata = [:] if let chef = Kitchen.chef { metadata["chef"] = "\(chef)" } return metadata } let metadataProvider = Logger.MetadataProvider.multiplex([orderMetadataProvider, chefMetadataProvider]) LoggingSystem.bootstrap(StreamLogHandler.standardOutput, metadataProvider: metadataProvider) let logger = Logger(label: "KitchenService")
-
18:13 - Logging with metadata providers
func makeSoup(order: Order) async throws -> Soup { logger.info("Preparing soup order") async let pot = stove.boilBroth() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) }
-
20:30 - Profile server-side execution
func makeSoup(order: Order) async throws -> Soup { try await withSpan("makeSoup(\(order.id)") { span in async let pot = stove.boilWater() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) } }
-
21:36 - Profiling server-side execution
func makeSoup(order: Order) async throws -> Soup { try await withSpan(#function) { span in span.attributes["kitchen.order.id"] = order.id async let pot = stove.boilWater() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.