concurrency의 사전적 정의는 ‘동시성'입니다. 한번에 여러 개의 작업을 동시에 수행하는 것을 의미합니다.
thread는 concurrency를 직접적으로 구현할 수 있게 해주는 개념입니다. 각각의 쓰레드는 한번에 하나의 작업을 담당해서 수행합니다.
만약 하나의 쓰레드만 존재한다면 아래 그림에 있는 작업을 하나의 쓰레드에서만 수행하게 되면 네트워크나 이미지 수정, 파일 저장을 하는 동안 사용자 interaction을 처리할 수 없을 것입니다. 사용성이 떨어지는 앱이 될 수 밖에 없습니다.
각 쓰레드는 실제도 동시에 작업을 수행하지는 않지만 빠른 속도로 번갈아 가면서 수행합니다. (아래 형광펜으로 표시된 쓰레드가 작업 중인 쓰레드를 표시한 것입니다^^) 사용자에게는 동시에 실행된다고 느껴질 정도입니다.
여러 개의 쓰레드 중에 가장 중요한 일을 담당하는 쓰레드를 메인 쓰레드라고 부릅니다. 앱에서 가장 중요한 것은 역시 사용자와의 interaction이겠죠? 메인 쓰레드는 사용자의 input을 받고 output을 UI에 표시하는 역할을 담당합니다.
메인 쓰레드는 가장 높은 우선순위를 가지기 때문에 사용자가 터치를 하면 바로 앱이 반응할 수 있습니다. 또한 코3드를 짤 때 UI 업데이트를 메인쓰레드가 아닌 다른 쓰레드에 넣게 되면 아래 그림처럼 에러가 발생합니다.
GCD는 Concurrency, 멀티쓰레딩을 iOS에서 쉽게 사용하기 위해서 한 단계 추상화한 개념입니다.
실행해야할 작업을 코드 블럭에 담아서 GCD에 전달하면 시스템이 알아서 쓰레드에 할당하고 실행해줍니다. 따라서 개발자가 직접 쓰레드에 접근할 필요가 없고 GCD를 잘 활용하면 됩니다.
GCD를 사용하기 위해서는 Dispatch Queue에 작업을 보내면 됩니다.
메인 쓰레드에서 작동하는 큐입니다. 주로 UI 업데이트와 사용자 input을 처리합니다. 단지 메인 쓰레드를 명시적으로 사용하지 않더라도 다른 큐에 보내지 않는 작업은 모두 메인 큐에 보내지게 됩니다.
DispatchQueue.main.async {
// 메인 쓰레드에서 실행할 작업
}
시스템에 의해서 관리되는 큐입니다. 작업의 중요도를 qos class를 통해서 정의할 수 있습니다. qos는 Quality Of Service를 의미합니다. 중요도는 아래 표를 참고해주세요.
DispatchQueue.global(qos: .userInteractive).async {
}
DispatchQueue.global(qos: .userInitiated).async {
}
DispatchQueue.global(qos: .default).async {
}
DispatchQueue.global().async {
//✅ qos를 명시 안하면 default
}
DispatchQueue.global(qos: .utility).async {
}
DispatchQueue.global(qos: .background).async {
}
직접 중요도와 이름을 지정해서 사용할 수 있는 큐입니다. 기본적으로 Serial Queue이지만 Concurrent Queue로 지정할 수도 있습니다.
let customConcurrentQueue = DispatchQueue(label: "customConcurrentQueue", qos: .background, attributes: .concurrent)
let customSerialQueue = DispatchQueue(label: "customSerialQueue", qos: .background)
해당 큐에 보내는 작업을 동기적으로 실행할 것인지 비동기적으로 실행할 것인지 결정합니다. 동기적으로 실행하는 경우 해당 작업을 모두 실행하고 다음 줄의 작업을 실행합니다. 반대로 비동기적으로 실행하는 경우 해당 작업을 실행하면서 다음 줄의 코드도 동시에 실행합니다. playground에서 아래 코드를 실행해보면서 예시를 들어보겠습니다.
//비동기적 실행
DispatchQueue.global(qos: .background).async {
for i in 0...5 {
print("먼저 실행 \(i)")
}
}
DispatchQueue.global(qos: .userInteractive).async {
for i in 0...5 {
print("나중에 실행 \(i)")
}
}
// 실행 결과
먼저 실행 0
나중에 실행 0
나중에 실행 1
나중에 실행 2
나중에 실행 3
나중에 실행 4
나중에 실행 5
먼저 실행 1
먼저 실행 2
먼저 실행 3
먼저 실행 4
먼저 실행 5
비동기적으로 실행을 하면 먼저 실행한 코드의 print 문이 다 나오기 전에 다음 코드의 print문이 실행 되는 것을 볼 수 있습니다. 그리고 두번째 코드의 DispatchQueue 우선 순위가 더 높으므로 나중에 실행한 print문이 먼저 출력됩니다.
// 동기적 실행
DispatchQueue.global(qos: .background).sync {
for i in 0...5 {
print("먼저 실행 \(i)")
}
}
DispatchQueue.global(qos: .userInteractive).async {
for i in 0...5 {
print("나중에 실행 \(i)")
}
}
// 실행 결과
먼저 실행 0
먼저 실행 1
먼저 실행 2
먼저 실행 3
먼저 실행 4
먼저 실행 5
나중에 실행 0
나중에 실행 1
나중에 실행 2
나중에 실행 3
나중에 실행 4
나중에 실행 5
반면에 동기적으로 실행하면 먼저 실행한 print문이 다 출력되기 전에는 다음 줄의 코드가 실행되지 않습니다. 이 때는 우선 순위에 관계 없이 무조건 먼저 실행한 작업이 끝나고 나서 다음 코드를 실행합니다.