본문 바로가기
Swift/Basic

Concurrency

by songmoro 2024. 11. 7.
728x90

Concurrency

  • 비동기 및 병렬 코드를 작성하기 위한 방법
  • GCD & Completion Handler
  • Async & Await

 

Async & Await 등장 이전

  • Completion Handler의 중첩으로 Callback Hell 발생
    • Callback: 작업이 완료된 후 실행되는 함수 또는 클로저
      • 비동기 작업 결과 처리 혹은 특정 작업 후 추가 동작을 위해 사용
    • Completion Handler: 비동기 함수에서 특정 작업이 완료된 후에 실행할 콜백 함수 또는 클로저
      • 함수는 작업을 시작한 후 반환 되지만, 클로저는 함수가 반환된 후 호출되기 때문에 클로저를 나중에 호출하기 위해 @escaping 어노테이션을 채택
    • Callback Hell 문제점
      • 중첩된 들여 쓰기로 인해 가독성 저하
      • 코드의 흐름이 직관적이지 않음
      • 휴먼 에러 증가
        • Completion Handler를 사용하지 않아도 에러 발생하지 않아 누락할 수 있음
        • self capture로 인한 retain cycle 발생 가능
  • 스레드 풀을 사용하여 병렬 작업 수행
    • 특정 작업이 완료되는 걸 기다리지 않아서 데이터 레이스 발생
  • 공유 자원에 대한 접근, 동기화 때문에 race condition 혹은 deadlock이 발생할 수 있어 semaphore, mutex와 같은 동기화 메커니즘을 사용해야 함

 

Async & Await

  • Completion Handler 대신 async, await 키워드를 사용해 좀 더 가독성 높게 비동기 프로그래밍 작성 가능
    • 비동기 함수를 동기 함수처럼 작성할 수 있어 가독성 향상
    • async 함수는 반드시 에러, 값을 반환해서 컴파일 시점에 오류 판독
  • 비동기 함수, 메서드
    • 실행 도중 중단될 수 있는 함수, 메서드
    • 비동기 함수, 메서드를 정의할 때 async 키워드 사용
    • 비동기 함수, 메서드를 사용할 때 await 키워드 사용
      • await은 Potential Suspension Point
        • 잠재적인 일시 중지 지점
  • 호출 시
    • await 키워드로 async 함수, 메서드를 만나면 스레드 제어권 포기
      • 이후 코드는 실행되지 않고 일시 중지(대기)
      • 스레드 양보(yielding)
    • 시스템이 작업 스케쥴링
      • 만약, 더 중요한 작업이 있다면 해당 스레드에서 중요한 작업 수행
    • async 함수를 수행할 차례가 되면 resume
      • 이 때 작업을 수행하는 스레드는 이전과 다를 수 있음
  • 코드 블럭이 하나의 transaction으로 처리되지 않을 수 있다.
    • 일시 중지되는 동안 앱의 상태가 크게 변할 수 있음

 

Continuation

  • 일반적으로 동기 함수 호출에선 함수의 로컬 변수와 반환 주소 및 기타 정보가 스택 프레임에 저장
    • 스레드가 함수 호출을 실행하면 새 프레임이 스택 프레임에 푸시되고, 완료하면 팝 된다.
    • 스레드는 독립적인 스택 영역을 보유, 힙을 통해 공유 데이터 사용
      • 스택은 함수 호출 상태를 저장하기 위해 사용
    • 기존의 동시성 프로그래밍을 위해선 여러 개의 스레드 사용이 필요
      • 여러 개의 스레드 작업을 번갈아가며 처리할 때 컨텍스트 스위칭 발생
  • Concurrency에서는 await을 만나면 해당 시점의 함수 상태를 저장
    • 여기서 Continuation이 등장
  • Continuation은 현재 함수의 상태(변수, 연산 상태, 호출 위치, …)인 함수 컨텍스트를 저장하는 경량 객체
    • 비동기 함수의 실행을 스택과 힙에서 관리
    • 스택에는 비동기 함수에 필요하지 않은 로컬 변수 저장, 힙에는 Suspension Point에서 실행하는 데 필요한 함수의 상태를 저장
      • 이게 Continuation
      • Continuation을 통해 일시정지된 함수의 상태를 추적해 재개할 지점을 알 수 있다.
    • Continuation은 힙에 저장되서 스레드간 공유 가능
  • 로직
    • 사용 될 가능성 있는 변수들을 Continuation 형태로 힙에 저장
    • Suspend 되었던 함수가 Resume되면, 스택 최상단 프레임이 해당 함수 프레임으로 교체됨
      • 이미 힙에 함수의 상태가 저장되어 있어서 새로운 스택 프레임을 생성하지 않고 교체해도 동작
    • 교체가 이루어지면, 이미 생성된 스레드의 재사용이 가능
      • 비동기 함수가 어떤 스레드에서 Suspend 되었던 간에, 효율적으로 실행될 수 있는 스레드에서 함수 컨텍스트를 불러와 Resume 가능
  • 수행
    • 함수가 await을 만나 일시 정지되면, 현재 함수의 상태는 Continuation을 통해 힙에 보관
    • 작업이 완료되면 Continuation에서 저장된 상태를 복원해 다시 함수 진행
    • 작업이 재개되는 스레드는 이전과 다른 스레드일 수 있다.
      • Continuation에 저장된 정보가 있어서 어느 스레드든 작업 재개 가능
  • 이와 같은 유연한 스레드 관리 방식이 Concurrency에서 도입된 Cooperative Thread Pool 개념
  • Concurrency의 비동기 작업은 스레드 전환이 아닌 Continuation 전환
    • 컨텍스트 스위칭보다 비용이 적음
      • 단순 함수 호출 비용만 소비
  • Concurrency는 Task에 대해 스레드를 생성하지 않음
  • Thread Blocking
    • 만약 비동기 작업이 오래 지속된다면 그동안 스레드는 작업을 기다리고 있음
      • A 함수에서 B라는 동기 함수를 호출하면, A 함수가 실행되던 스레드의 제어권을 B 함수에게 전달
      • 그래서 B 함수가 끝날 때 까지 해당 스레드는 점유되어 다른 일을 수행하지 못함
      • B 함수가 끝나면 A 함수에 스레드 제어권 반환
    • Concurrency는 Continuation을 통해 스레드가 blocking 되지 않고, 필요하다면 다른 작업 수행 가능

 

GCD, Async & Await 비교

  • 우선순위 역전
  • 스레드 블록
    • 컨텍스트 스위칭
  • 스레드 익스플로전
  • 휴먼 에러

 

Task

  • 비동기 작업의 단위
  • Dispatch Queue와 비교
    • 공통점
      • 임의의 스레드로 처리
    • 차이점
      • Task 블록 내부에서 await을 만나면 일시 정지하여 다음 코드를 실행하지 않지만, 내부적으로 스레드는 block 처리되지 않아 다른 일을 처리할 수 있음
      • Dispatch Queue는 해당 스레드가 점유하고 작업을 진행 중이면, 스레드는 block 되어 다른 일을 처리하지 못함

 

Actor

  • 동시성 프로그래밍에서 주의할 점인 Deadlock, Race Condition을 해결하기 위한 자료형
  • Reference 타입이며, 한 번에 하나의 작업만 Actor의 상태를 변경할 수 있음
    • 내부적으로 mutex 처리
  • nonisolated 키워드
    • actor는 let 키워드로 선언하여 읽기만 가능
      • 그로 인해 race condition 보장
    • 메소드 앞에 nonisolated를 붙여 사용할 때 Task, await 키워드 없이 접근 제공
  • @MainActor 키워드
    • 항상 main에서 동작하는 Actor
    • 클래스 혹은 메소드에 채택

 

 

728x90

'Swift > Basic' 카테고리의 다른 글

Generic, Protocol  (2) 2024.11.07
Data Type  (0) 2024.11.07
GCD  (0) 2024.10.31
Memory Management  (0) 2024.10.31
Memory  (0) 2024.10.31