article thumbnail image
Published 2022. 12. 30. 18:19

동시성 프로그래밍이란 "작업들을 동시에 처리하는 프로그래밍"을 의미한다. 

이는 병렬 프로그래밍과 다른 개념인데, 

동시성 프로그래밍은 하나의 processor가 A 작업과 B 작업을 왔다갔다 하며 동시에 처리하는 반면,  

병렬 프로그래밍은 두개 이상의 processor가 A와 B를 하나씩 맡아 동시에 처리하는 것이다.

 

processor에서 multi-core를 활용하기 위해,

software에선 "수행해야 할 작업(Task)들을 동시에 처리"하는 동시성 프로그래밍이 필요하다.

 

동시성 프로그래밍은 크게 3가지를 통해 이루어질 수 있는데, 이번 포스팅에서는 이중 "멀티 쓰레딩"을 알아볼 것이다.

이에 앞서, Thread 개념에 대해 간단하게 알아보자.

 

 

Thread

Thread에 대해 이해하기 앞서 간단하게 Process에 대해 알아보아야 하는데, 

 

ProcessOS로부터 자원(메모리)이 할당된 Task(작업)의 단위이다.

또한, 메모리 영역을 독립적으로 할당받게 되며,  process간의 메모리 영역으로는 접근이 불가하다. 

 

Thread한 Process 안에서 실행되는 작업(Task) 흐름의 단위를 의미하며 Process의 특정 메모리 영역을 공유한다.

Thread Stack 영역만 할당받고  Process의 Code, Data, Heap영역을 공유한다. 

 

 

CPU는  한순간에 하나의 Task(작업)만을 실행하기에, 진행할 Task(작업)의 메모리 영역을 불러와야 한다.

이러한 절차를 Context Switching이라고 부른다.

 

MultiProcess의 경우부터 알아보자.

보통 하나의 프로그램은 여러 Process로 구성되어 있는데, 

각각 Process는 독립적인 메모리 공간을 할당받으며, 서로의 메모리 영억으로 접근이 불가능하기에

Context Swtiching이 일어날 경우, 오버헤드가 크다. 

 

MultiThread의 경우에는, (Process내에 여러 개의 Thread가 있다면)

Code, Data, Heap영역은 다른 Thread와 공유하게 되고, 

각 Thread는 Stack 영역만 할당받게 된다.

즉, 모든 메모리 영역이 다 독립적MultiProcess에 비해, 

특정 메모리 영역을 공유하는 MultiThreadContext Switching이 빠르다.

 

 

iOS에서는 GCD, NSOperation를 통해 멀티쓰레딩을 통한 동시성 프로그래밍을 지원하며,

유저가 직접 Thread를 스케쥴링하는 것이 아닌,  "대기행렬(큐)"에 넣어주기만 하면, OS가 알아서 다른 스레드로 분산처리 해주게 된다.

 

이렇게 3개의 Task(작업)이 Main Thread에 위치해있다고 하자

아래와 같이 "대기행렬(큐)"에 작업을 넣어주게 되면, 

OS가 알아서 Queue에 들어있는 작업을 분산 처리를 진행한다. 

 

 

동기(Synchronous) vs. 비동기(Asynchronous)  

작업(Task)은 요청과 응답으로 이루어져 있는데, 응답의 기다림의 유무로 동기(Sync)와 비동기(Async)를 나눌 수 있다.

Synchronous요청을 보내고 응답이 올 때까지 기다리는 것이다. 

즉, 해당 작업이 끝날 때까지 기다리게 된다. 

Task1이 완료될 때까지 Task2는 실행되지 못하고, 대기상태에 빠지게 된다. 사실상 이는 Queue에 보내지 않고 Main Thread 처리하는 것과 같다.

 

 

// 시간 측정을 위한 함수
public func progressTime(_ closure: () -> ()) -> TimeInterval {
    let start = CFAbsoluteTimeGetCurrent()
    closure()
    let diff = CFAbsoluteTimeGetCurrent() - start
    return (diff)
}

// Task 함수
func someTask(_ taskNumber: Int) {
    print("Task\(taskNumber) Start")
    sleep(3)
    print("Task\(taskNumber) Finish")
}

let time = progressTime {
    someTask(1)
    someTask(2)
}
print(time)

/* Prints:
 Task1 Start
 Task1 Finish
 Task2 Start
 Task2 Finish
 6.003826022148132
*/

 

 

반면, Asynchronous요청에 대한 응답을 기다리지 않고, 다른 작업을 수행하는 것이다.

즉, 해당 작업이 끝날 때까지 기다리지 않고 즉시 리턴하여 다음 Task가 바로 실행될 수 있다.

 

Sync와 Async를 코드상에서 비교해 보자. 

//시간 측정을 위한 함수
public func progressTime(_ closure: () -> ()) -> TimeInterval {
    let start = CFAbsoluteTimeGetCurrent()
    closure()
    let diff = CFAbsoluteTimeGetCurrent() - start
    return (diff)
}


func someTask(_ taskNumber: Int) {
    print("Task\(taskNumber) Start")
    sleep(3)
    print("Task\(taskNumber) Finish")
}

동기의 경우, 

// Swift에서 default는 Main Thread에서 동기적으로 작동한다.

let time = progressTime {
    someTask(1)
    someTask(2)
}

print(time)

/* Prints:
 Task1 Start
 Task1 Finish
 Task2 Start
 Task2 Finish
 6.003826022148132
*/

someTask(1)이 끝날 때까지, someTask(2)는 실행되지 못하는 것을 볼 수 있다. 

 

비동기의 경우, 

let time = progressTime {
    DispatchQueue.global().async {
        someTask(1)
    }
    someTask(2)
}
print(time)

/* prints:
 Task2 Start
 Task1 Start
 Task1 Finish
 Task2 Finish
 3.0037219524383545
*/

 

someTask(1)이 비동기적으로 실행이 되기 때문에, 

someTask(2)와 거의 동시적으로 실행되는 모습을 볼 수 있다. 

 

해당 결과에서 Task2가 먼저 시작되는 모습을 보이는데,

이는 DispatchQueue.global().async의 클로져 부분이 task의 단위이기 때문이다.

 

직렬(Serial) vs.  동시(Concurrent)

SerialConcurrent 큐의 특성이다.

 

Queue에 다음과 같이 3개의 Task가 있다고 하자.

 

Serial Queue Queue내부의 작업들을 하나의 Thread에서 순차적(직렬)으로 수행한다. 

이는 실행 순서가 중요한 작업을 진행할 때, 사용한다.

 

반면, Concurrent Queue Queue내부의 작업들을 여러 Thread에서 동시다발(병렬)적 수행한다.

 

 

Reference 

Dispatch

DispatchQueue

Concurrency and Application Design

https://younggyun.tistory.com/21

복사했습니다!