Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[승용] 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

AltStyle によって変換されたページ (->オリジナル) /