Skip to content

[승용] Explore structured concurrency in Swift

Eric Kwon / 권승용 edited this page Aug 18, 2024 · 1 revision

개요

  • 처음 프로그래밍이 나왔을 떈 코드를 읽기 어려웠음

    • 명령어의 시퀀스로 이루어져 있어 제어 흐름이 여기저기로 이동함
  • 요즘은 그런걸 보기 힘듬

    • 구조화된 프로그래밍을 사용해 제어 흐름을 더욱 단일화시킴
    • ex) if-then
  • 구조화된 프로그래밍은 자연스럽게 nested 및 sequenced 될 수 있다.

    • 이는 프로그램을 위에서 아래로 자연스럽게 읽을 수 있도록 도와줌
  • 이것들이 구조화된 프로그래밍의 중요한 요소들!

  • 그러나 요즘 프로그램들은 비동기적이고 동시적인 코드들을 제공하는데 이들은 구조화된 프로그래밍을 사용해 더 쉽게 코드를 작성할 수 없었다.

  • unstructured 비동기적 코드 예시

    Screenshot 2024-08-18 at 3 57 46 AM
    • 에러 핸들링 사용 불가
    • 반복문 사용 불가
  • async-await을 사용해 개선

    Screenshot 2024-08-18 at 3 58 49 AM
    • 에러 핸들링 사용 가능
    • 반복문 사용 가능
    • 반환하는 값이 있음

Task

  • asyn-await으로 비동기적 작업은 개선했는데, 여러 개의 작업을 동시적으로 작업하고 싶다면?
  • Task 사용
  • 각 task들은 다른 실행 흐름과 동시적이게 실행되고, 안전하고 효율적일 때 자동적으로 실행되도록 시스템에 의해 스케줄된다.
  • Swift에 깊이 통합되어 있기 때문에 컴파일러가 동시성 문제를 감지할 수 있음
  • async 함수를 호출한다고 task가 생성되는 것이 아님.
    • Task 호출을 통해 명시적으로 task를 생성함

Concurrent binding

  • async let
    • await 비동기적 작업을 concurrent하게 실행하고 싶을 때 사용 가능 Screenshot 2024-08-18 at 11 59 50 AM

    • binding을 만나면 swift는 child task를 생성

    • child task는 데이터 다운로드 즉시 시작

    • parent task는 result 변수에 임시 값(placeholder)으로 즉시 바인딩

    • 이 parent task는 이전에 진행되던 작업과 동일한 흐름이기 때문에 그 아래 구문들을 이어서 실행함

    • 그러나 임시 값을 대체할 실제 값이 필요한 구문에서는 child task의 완료를 기다림

  • 적용 전
Screenshot 2024-08-18 at 11 58 51 AM
  • 적용 후
Screenshot 2024-08-18 at 12 05 22 PM
  • try awiat은 실제 변수를 사용할 때에만 적용하면 됨

Task Tree

  • Swift에서 생성되는 모든 child task는 Task Tree라는 계층구조의 일부이다.
  • 이는 단순한 내부 구현 사항이 아닌, structured concurrency의 중요한 개념이다.
  • task tree는 task의 속성(cancellation, 우선순위, task-local 변수 등)에 영향을 미친다.
  • async 함수에서 다른async 함수를 호출할 때에는 동일한 Task가 호출을 실행하기 위해 사용된다.
Screenshot 2024-08-18 at 12 48 39 PM
  • 따라서 fetchOneThumbnail 함수는 해당 Task의 모든 attribute들을 상속받는다.
  • async-let 등을 사용해 새로운 structured task를 생성하면, 현재 함수가 실행되고 있는 task의 child task가 된다.
  • Task는 특정 함수의 child는 아니지만, lifetime이 함수의 범위에 묶여 있을 수는 있다.
  • Task Tree는 parent task와 각 child task와의 연결로 이루어져 있다.
Screenshot 2024-08-18 at 1 05 08 PM
  • 이 연결은 다음과 같은 규칙을 강제한다:
    • 부모 태스크는 자식 태스크가 모두 완료되기 전에는 작업을 끝낼 수 없다.
    • 이 규칙은 child task가 await되지 않는 비정상적인 흐름에서도 유지된다.
  • 위 코드에서 metadata를 먼저 await하고, data를 나중에 await한다고 가정했을 때 metadata task가 오류를 던지면서 완료되면 fetchOneThumbnail 함수도 즉시 오류를 던지고 종료해야 한다.
    • 그렇다면 data task는 어떻게 될까?
  • 비정상적인 종료가 발생하면 Swift는 자동으로 await되지 않은 task를 canceled로 표시하고, 종료 전에 해당 task가 완료되기를 기다린다.
    • task의 cancel이 task의 중지를 의미하지는 않는다.
      • task에게 그 결과가 더이상 필요하지 않음을 알리는 신호만을 보낸다.
    • cacel되는 task의 자손 task들도 자동적으로 cancel된다.
  • 따라서 fetchOnceThumbnail이 생성한 모든 structured task들이 완료된 후에 에러를 던지며 종료될 것이다.
    • 이는 structured concurrency의 핵심 개념으로, task 누수를 방지하고 task의 수명을 자동으로 관리한다.
    • 이는 ARC가 자동으로 메모리를 관리하는 방식과 유사하다.

그럼 태스크는 언제 완전히 멈추는가?

  • Swift의 task cancellation은 협력적(cooperative)인 방식으로 이루어진다.
  • 코드가 명시적으로 취소 상태를 확인하고 상황에 맞게 실행을 정리해야 한다.
  • cancellation을 염두에 두고 코드를 작성해야 한다.
  • task는 취소되는 순간에 즉시 멈추지 않는다.
  • task의 취소는 어디서든 확인 가능하다.
Screenshot 2024-08-18 at 2 32 05 PM Screenshot 2024-08-18 at 2 31 56 PM
  • 따라서 반복적으로 이미지를 다운로드하는 코드에서 cancellation이 발생한다면 그 이미지를 다운로드하지 않고 건너뛰는 코드를 명시적으로 추가해줄 수 있다.
  • 여기까지 Async-let task에 대해 알아보았다.

Group task

  • async-let보다 더 유연하다.
  • async-let은 고정된 크기의 concurrency가 있을 때 잘 작동하고, 변수처럼 범위 내에서만 동작한다.
  • 위 예시의 fetchOneThumbnail은 하나의 id당 두 개의 child task를 생성한다.
    • 이러한 task들은 다음 반복이 시작되기 전에 완료되어야 한다.
  • 그러나 동적 동시성, 즉 ID 배열 크기에 따라 동시성 수준을 조정하고 싶다면 Task Group을 사용하는 것이 좋음
  • task group은 동적인 갯수의 concurrency를 제공하기 위해 설계된 structured concurrency의 한 형태
  • withThrowingTaskGroup 함수를 사용해 작업 그룹 생성 가능
Screenshot 2024-08-18 at 5 19 56 PM
  • 이 작업 그룹이 정의된 범위를 벗어나면 모든 작업이 자동으로 완료될 때 가지 기다린다.
    • group task들의 child task들이 생성되고... 그것들이 모두 끝날 때 까지 기다린 후 thumbnails를 return할 수 있음
  • 다만 위 코드는 경쟁 조건 문제가 있음
  • thumbnails에 동시적으로 여러 스레드들이 접근해 작업 가능
  • 아래와 같이 개선할 수 있다:
Screenshot 2024-08-18 at 5 23 34 PM
  • 타입이 AsyncSequence를 준수한다면 for-await을 사용해 반복 가능

  • Task group은 structured concurrency의 한 형태이지만 async-let과는 약간 다르게 동작한다.

  • 만약 task group 안에서 실행 중인 task 중 하나가 오류를 발생시킨다면 이 오류는 그룹 블럭 밖으로 throw되고 그룹 내의 다른 모든 task는 암시적으로(implicitly) cancel된 후 완료될 때 까지 기다리게 된다.

    • 이 부분은 async-let과 똑같이 동작
  • 그러나 task group이 정상적으로 블록을 빠져나갈 때는 cancellation이 implicit하게 일어나지 않는다.

    • 이는 fork-join(분기 후 결합) 패턴을 표현하기 쉽게 만들어준다.
    • 작업들은 await될 뿐 cancel되지 않는다.
  • group의 cancelAll 메소드를 사용해 수동으로 모든 작업을 취소할 수 있음

Unstructured tasks

  • 모든 작업들이 structured pattern에 들어맞지는 않음
    • non-async 코드에서 task가 시작될 경우
    • task를 시작할 때 부모 task가 없는 경우
    • task의 수명이 단일 범위 또는 단일 함수의 제한이 맞지 않는 경우
      • 객체를 활성화하는 메소드에 task를 시작하고, 객체를 비활성화하는 다른 메소드 호출에 task를 종료하고 싶을 수도 있음
  • 예시
Screenshot 2024-08-18 at 5 40 13 PM
  • collection뷰의 델리게이트에서 fetchThumbnail 함수를 사용하고 싶음
  • 그런데 UI와 관련된 코드이기 때문에 @MainActor 적용해 메인 스레드에서 실행되는 코드
  • 여기서 await을 그냥 사용할 수는 없음 - 오류 발생
  • Swift는 이러한 상황을 위해 unstructured task 생성을 지원함
Screenshot 2024-08-18 at 5 40 59 PM
  • 런타임에 Task를 만나면 Swift는 task를 원래 범위와 같은 actor에서 실행되도록 스케줄링한다.
  • 한편 제어권은 즉시 호출자에게 반환된다.
  • fetchThumbnails 태스크는 메인 스레드에서 실행되지만, 델리게이트에서 메인 스레드를 블록하지 않고 실행할 기회가 있을 때 실행된다.
  • unstructured task의 특징
    • 시작 context의 actor를 상속받음
    • 원본 task의 우선순위와 기타 특성을 상속받음
    • 범위가 지정되지 않음(unscoped)
    • task가 시작된 범위(scope)에 수명이 제한받지 않음
    • non-async 함수 등 어느 곳에서나 생성 가능
    • cancellation 및 에러 전파와 await을 수동으로 관리해야 함
  • 아래는 스크롤 범위 바깥으로 벗어난 thumbnail fetch task들을 명시적으로 cancel하는 예시
Screenshot 2024-08-18 at 5 48 10 PM Screenshot 2024-08-18 at 5 48 27 PM

Detached tasks

  • 상위 task로부터 아무런 속성도 상속받고 싶지 않을 때

  • context로부터 완전히 독립적임

  • unstructured task의 일종

  • 시작된 범위에 수명이 묶이지 않음

  • 시작된 범위의 attribute(actor, 우선순위 등)을 상속받지 않음

  • 우선순위 등에 대해 기본값을 가지고 시작하지만 커스텀 매개변수를 넣어줄 수도 있음

  • 캐싱하는 detached Task 예시

    Screenshot 2024-08-18 at 5 51 53 PM
    • priority 설정 가능
    • thumbnail 실패해도 얘는 cancel 안 됨 -> 독립적인 실행
    • 따라서 fetch 실패해도 일단 캐싱은 해놓고 싶은 목적으로 사용할 때 적절함
  • detached task와 지금까지 살펴본 요소들 조합 가능

  • detached task 안에 structured task 적용 가능

Screenshot 2024-08-18 at 5 54 42 PM
  • 그러면 자동적으로 cancellation을 전파할 수 있고, priority를 하위 task들에게 전파할 수 있어 관리가 쉽다는 장점이 있음

정리

Screenshot 2024-08-18 at 5 57 57 PM
Clone this wiki locally