[Swift] Concurrency : Thread 집중탐구 + thread explosion

Young Bin Lee·2022년 7월 6일
0

Concurrency

목록 보기
1/1
post-custom-banner

모든 의문은 이 에서 시작되었다. 해당 포럼글의 요지는 serial 큐에서는 왜 thread explosion이 일어날 수
없냐는 것이다.

Swift의 GCD를 알아보고 그 중 특히 concurrent와 serial queue가 각각 가질 수 있는 문제점을 알아볼 것이다.
이 다음은 아마 Swift concurrency(aka async-await)에 관한 글이 될 것 같다.

Refs

Concurrent는 진짜 concurrent할까?

Concurrent 큐는 당연히 concurrent하게 작업을 처리한다. 무슨 말인가 하면 해당 큐에 등록된 작업들은 동시에 실행된다는 뜻이다. 작업은 쓰레드라는 단위로 처리된다. 이를 잘 정리해 보면 결국 concurrent 큐는 여러개의 쓰레드를 가질 수 있다는 결론이다.

코어(CPU)의 역할은 열심히 자신에게 주어진 쓰레드(작업)를 처리하는 것이다. 아이폰의 경우 1코어 1쓰레드기 때문에 하나의 코어에 한 개의 쓰레드만 처리하지만 요즘 나오는 인텔이나 AMD의 코어는 한 코어에서 여러개의 쓰레드를 동시 처리한다. Hyper-쓰레딩이라고 알려진 기술이다. 하지만 그렇지 못한 아이폰은 코어가 6개(아이폰 12 기준)기 때문에 이론상 동시에 처리할 수 있는 작업은 최대 6개가 되는 셈이다.

하지만 우리에게 동시에 6개의 작업밖에 하지 못하는 나약한 아이폰은 필요가 없다. 그리고 당연히 그러지도 않는다. 그 이유는 바로 컴퓨터가 Concurrent하게 일을 처리하기 때문이다.

알다시피 동시에 여러개의 작업들이 처리된다는 것은 사실 컴퓨터가 주는 환상이다. 실제로는 서로 다른 작업을 담당하는 쓰레드가 코어 내에서 아주 빠른 속도로 전환하고 있으며 그에 따라 우리는 끊김없이 작업들이 이어지고 있다는 착각을 하게 된다.

쓰레드가 너무 많아지면

그러다 보니 많은 일을 동시에 처리하려면 그만큼 쓰레드도 많아지게 된다. 동시에 100개의 이미지를 로드하는 작업을 Concurrent 큐에 올린다고 생각해보자. OS는 주어진 일을 열심히 해내고자 시스템이 허용하는 한도 내에서 최대한 많은 쓰레드를 생성해 작업하고자 할 것이다.

Thread explosion는 쓰레드가 과도하게 많아지는 현상을 일컫는다. 쓰레드가 많아지면 코어에서는 쓰레드 수 만큼의 context switchin이 일어난다. 쉽게 말해 100개의 thread가 있는 코어는 모든 작업을 마치기 위해 적어도 100번의 context switching을 해야 한다. 그리고 모두 알다시피 context switching은 상당한 오버헤드를 발생시켜 디바이스의 퍼포먼스를 저해한다.


WWDC21

그러면 쓰레드의 수를 시스템에서 제한하는 방법이 있을까? 첫째로 serial 큐에서 sync 함수를 쓰는 방법이 있다. Serial 큐는 동시에 작업을 처리하지 않고 sync는 쓰레드의 제어 권한을 돌려주지 않기 때문에 다른 쓰레드를 만들지 않는다. 다만 Serial큐라 하더라도 async를 사용하면 쓰레드가 새로 생성되므로 적절하지 않다.

그러면 아예 쓰레드의 수를 제한하는 방법은 어떨까? 이 아저씨의 소논문에 따르면 그건 불가능할 것 같단다.

그러면 우리는 어떻게 대응해야 할까. 이 때 단일 쓰레드에서 작업을 처리하는 Swift Concurrency, async-await이 필요하다. Swift Concurrency는 다음 글에 더 알아보도록 하고 지금은 Queue의 동작에 대해 조금 더 들여다보도록 하자.

큐를 만든다고 쓰레드가 늘어나지 않는다

우리가 이해해야 할 것이 있다. 큐를 새로 만든다고 쓰레드가 늘어나는 것이 아니다. 간단한 테스트를 통해 알아보자.

실험 1. 2개의 sync

import Foundation

let q = DispatchQueue(label: "Test")

q.sync {
    sleep(1)
    print("1", Thread.current)
}

DispatchQueue.main.async {
    print("2", Thread.current)
}

q.sync {
    sleep(1)
    print("3", Thread.current)
}

DispatchQueue.main.async {
    print("4", Thread.current)
}

결과 : 1324

희한하게도 결과는 1324다. Test 큐와 main 큐가 다른 큐라 서로 영향을 끼치지 않을 것 같지만 보다시피 Test 큐의 sleep이 모두 끝날 때까지 main 큐가 기다린다. 그 이유는 바로 둘 다 main 쓰레드에서 실행되기 때문이다.

이는 위의 참고자료에서 답을 엿볼 수 있다. serial queue의 sync는 새로운 스레드를 만들지 않고 실행된 쓰레드를 재사용한다.


실험 2. 2개의 async 안의 2개의 sync

굳이 새로운 쓰레드를 만들어 관리하고 싶다면 아래와 같이 다른 큐에 async하게 올리는 방법을 이용하면 된다.

let q = DispatchQueue(label: "Test")
let newQ = DispatchQueue(label: "Test2")

newQ.async {
    q.sync {
        sleep(1)
        print("1", Thread.current)
    }
}

DispatchQueue.main.async {
    print("2", Thread.current)
}

newQ.async {
    q.sync {
        sleep(1)
        print("3", Thread.current)
    }
}

DispatchQueue.main.async {
    print("4", Thread.current)
}

결과 : 2413

2개의 쓰레드를 사용 중인 것을 알 수 있다. async를 sync로 바꾸면 1번처럼 1324에 쓰레드 1개를 사용한다.


실험 3. 2개의 async

let q = DispatchQueue(label: "Test")

q.async {
    sleep(1)
    print("1", Thread.current)
}

DispatchQueue.main.async {
    print("2", Thread.current)
}


q.async {
    sleep(1)
    print("3", Thread.current)
}

DispatchQueue.main.async {
    print("4", Thread.current)
}

결과 : 2413

  • 2스레드

실험 4. sync -> async

let q = DispatchQueue(label: "Test")

q.sync {
    sleep(1)
    print("1", Thread.current)
}

DispatchQueue.main.async {
    print("2", Thread.current)
}


q.async {
    sleep(1)
    print("3", Thread.current)
}

DispatchQueue.main.async {
    print("4", Thread.current)
}

결과 : 1243

  • 2스레드. 그런데 조금 특이한.

실험 5. async -> sync

let q = DispatchQueue(label: "Test")

q.async {
    sleep(1)
    print("1", Thread.current)
}

DispatchQueue.main.async {
    print("2", Thread.current)
}


q.sync {
    sleep(1)
    print("3", Thread.current)
}

DispatchQueue.main.async {
    print("4", Thread.current)
}

결과 : 1324

  • 2스레드

  • 나름의 결론
    - 시리얼 큐의 async는 메인 스레드 이외의 스레드를 만든다
    - 중요한 개념!!! sync는 스레드를 재사용한다.
profile
I can make your dream come true
post-custom-banner

0개의 댓글