티스토리 뷰

스터디를 하다가 홍선생님께서 Swift Concurrency를 이해하기 위해 필요한 3가지 중 Continuation이 있다고 하셨다. 그래서 Continuation에 대해 찾아보다가 WWDC21의 Swift concurrency: Behind the scenes를 보게되었고 여기서 GCD와 Swift Concurrency의 차이점에 대해 설명해주는데 좀 흥미로워서 글로 남겨볼까 한다.

 

GCD (Grand Central Dispatch)

기존 GCD에서는 Task들을 여러 스레드에 적절히 분배해서 작업을 처리했다. 그래서 계속 스레드가 생기고, 이를 차단하고 Context Switching 하는 방법으로 작업을 수행했다. 그러나 만약, 스레드가 코어 수보다 너무 많아져서 스레드 폭발 이라고 불리는 현상이 일어나거나, 너무 많은 Context Switching이 발생하면 성능이 떨어지거나 deadlock의 가능성이 있다.

 

Swift Concurrency

그래서 Swift Concurrency에서는 스레드를 바꿔가며 Task를 처리하지 않고, 하나의 코어에 하나의 스레드만 배치한다. 그리고 작업이 다시 진행되는지를 추적하는 continuation이라는 객체를 이용한다. 그래서 작업이 전환될 때 더이상 thread context switch가 일어나는게 아니라, 그냥 continuation 간 전환만 일어나기 때문에 거의 함수 호출 비용정도밖에 들지 않게된다. (가벼워진다.)

 

Non-blocking Thread

Swift Concurrency의 await는 현재 작업하는 thread를 차단(blocking)하는 것이 아니라, non-blocking 방식으로 작업을 suspend(중단)시키는데, 어떻게 하는 것일까?

 

1. async 함수가 아닌것

각 스레드는 함수 요청 상태를 저장하는 스택을 갖는다. 그래서 함수가 호출되면 해당 함수가 스택에 쌓이게 되고, 해당 스택 내에는 반환될 주소, 함수 내 지역변수들을 가지고 있는다. 함수가 종료되면, 스택에서 pop된다. async함수가 아닌 일반 동기함수인 경우 다음과 같은 방식으로 동작한다.

 

2. async 함수인 경우, non-blocking한 thread 작동 방식

func save(_ newArticles: [Article], for feed: Feed) async throws -> [ID] { /* ... */ }

// on Feed
func add(_ newArticles: [Article]) async throws {
    let ids = try await database.save(newArticles, for: self)
    for (id, article) in zip(ids, newArticles) {
        articles[id] = article
    }
}

func updateDatabase(with articles: [Article], for feed: Feed) async throws {
    // skip old articles ...
    try await feed.add(articles)
}

Swift Concurrency에서는 stack뿐 아니라 heap이 사용된다. 이때 Stack frame에는 suspension point에서 필요하지 않은 local 변수를 저장하고, heap에는 suspension point에서 실행하는데에 필요한 함수 컨텍스트가 저장된다. heap에 저장된 것 자체를 continuation이라고 한다. 

 

updateDatabase()에서 await feed.add(articles)가 실행되면, add()가 스택에 쌓이고, 내부에는 suspension point에서 사용되지 않는 (id, article)이 저장된다. 

그리고 heap에는 (continuation) Suspension point 전후로 사용되는 newArticles가 저장된다.

add() 내부에서 try await database.save(newArticles, for:self)를 만나면 save()가 sync 함수처럼 add() 스택 위에 쌓이는 것이 아니라, 아예 add -> save로 대체된다. 그래서 새로운 stack frame을 만들지 않아도 된다.

save() 함수도 async함수니까 안에 await를 포함하고 있을 것이다. await를 만나면, save()는 suspend되고, 아예 다른 작업이 스레드를 사용할 수 있게 한다. (스레드가 차단되지 않고 계속 사용될 수 있게 한다)

 

중단되었던 save()가 다시 재개되면, stack으로 돌아오는데, 이때 스택이 이전에 실행되었던 스택인지는 모른다. 남아있는 스택에 알아서 스레드 제어권을 가진 시스템이 다시 데려오게 된다. 그래서 작업이 resume되는 스레드는 이전에 실행되었던 스레드와는 다를 수 있다.

 

그리고 동시함수인 zip()을 만나면, stack 프레임이 새로 생기고, push된다.

 

기존 Completion 코드를 Async를 이용하여 바꾸는 법

swift 에서 제공하는 continuation API를 이용해서 바꿀 수 있다. 제공하는 Continuation API는 UnsafeContinuation과 CheckedContinuation이 있다.

 

Xcode에서는 자동으로 바꿔준다! .. 

플젝에 열심히 바꿔봐야겠다..

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함