이전 글에서 동시성에 대해서 알아보았다. 하지만 iOS에서는 Thread를 직접 생성해서 작업하지 않는다. 그러면 어떻게 동시성 프로그래밍을 가능하게 할까? 그 답인 GCD에 대해서 알아보자. 시작해보자.
Apple이 개발한 기술로, 멀티 코어 프로세스 환경에서의 애플리케이션 지원을 최적화하기 위해서 개발되었다.
여기서 시스템이 관리하는 Queue가 있는데, 이걸 Dispatch Queue라 한다. 이 Queue에 하고 싶은 작업을 정의해서 여기에 추가하기만 하면 된다.
생성 방법으로는 2개가 있다. 바로 main queue를 사용하거나, custom queue를 사용하거나.
DispatchQueue.main
DispatchQueue(label: "customSerial")
여기서 해당 Queue를 동기적으로, 비동기적으로 수행하도록 옵션을 걸 수 있다.
let queue = DispatchQueue(label: "SerialSyncQueue")
queue.sync {
print("task 1")
}
queue.sync {
print("task 2")
}
queue.sync {
print("task 3")
}
print("Done!")
/*
Result
task 1
task 2
task 3
Done
*/
let queue = DispatchQueue.main
queue.async {
print("task 1")
}
queue.async {
print("task 2")
}
queue.async {
print("task 3")
}
print("Done???")
/*
Done??
Result
task 1
task 2
task 3
*/
async
외에도 asyncAfter
메서드도 있다.
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
print("Executes after 2 seconds")
})
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 2.0, execute: {
print("Executes after 2 seconds")
})
생성 방법은 역시 2개가 있다.
DispatchQueue.global()
DispatchQueue(label: "CustomConcurrentQueue", attributes: .concurrent)
let queue = DispatchQueue.global()
queue.sync {
print("task 1")
}
queue.sync {
print("task 2")
}
queue.sync {
print("task 3")
}
print("Done!")
/*
Result
task 1
task 2
task 3
Done
*/
concurrent queue로 작업들이 들어가게 되고, concurrent queue에 sync로 작업이 들어갔기 때문에 각각의 task가 서로 다른 물리적 코어에도 작동하더라도(우연히 같은 코어일수도 있음) 각 결과를 받은 후에 동작한다.
let queue = DispatchQueue.global()
queue.async {
print("task 1")
}
queue.async {
print("task 2")
}
queue.async {
print("task 3")
}
print("Done???")
/*
Result
task 2
task 1
task 3
Done???
*/
현재 결과는 이렇지만, 각 task가 queue에 들어간 후, 시스템 환경과 현재 코어 활성도에 따라 동작하는 순서가 달라진다. 즉 결과를 보장받을 수 없다.
Serial Queue(async) + Main Queue
let queue = DispatchQueue(label: "serial.queue")
queue.async {
// Do Some Long-Running Task
DispatchQueue.main.async {
// Update UI
}
}
Concurrent Queue(async) + Main Queue
DispathQueue.global().async {
// Do Some Long-Running Task
DispatchQueue.main.async {
// Update UI
}
}
Apple에서 제공하는 작업의 명시적인 분류
이 분류는 작업 스케쥴링 시, 작업의 타입에 따라 우선순위를 다르게 하기 위함이다. 아래는 우선 순위에 따른 분류이다. 우선순위가 높을 수록 더 많은 리소스로 더 빠르게 수행한다. 지정은 Queue를 만들 때, 파라미터로 입력 가능하다.
QoS를 지정하지 않으면 default로 지정되는데, default는 User Initiated와 Utility 사이에 존재하는 중요도를 가진다.
DispatchQueue 또는 DispatchGroup 안에서 수행할 작업을 캡슐화 함
item내에서 실행가능하고, dispatchQueue로 넘겨서 실행이 가능하다.
let item = DispatchWorkItem(block: {
print("task?")
})
item.perform() // 현재 스레드에서 작업을 동기적으로 수행
item내에서 perform으로 수행하는 경우, 동기적으로 수행하기 때문에 작업 결과를 기다려야 한다.
let item = DispatchWorkItem(qos: .utility, block: {
print("task")
})
DispatchQueue.global().async(execute: item)
혹은 이렇게 Queue안에 item 자체를 넣어서 실행도 가능하다. 이경우는 Queue를 지정하기 때문에 결과를 기다리지 않아도 된다.
let item = DispatchWorkItem(block: {
for _ in 0..<100 {
print("task")
}
})
DispatchQueue.global().async(execute: item)
item.wait()
// 작업 끝날 때까지 기다림
print("Done!")
작업을 global queue로 넘기고 제어권을 넘겨받은 상태에서도 넘긴 작업 (다른 스레드에서 작업중)을 기다릴 수 있다. wait
메서드를 사용하면 된다.
let item = DispatchWorkItem(block: {
for _ in 0..<100 {
print("task")
}
})
DispatchQueue.global().async(execute: item)
item.notify(queue: .main, execute: {
print("Done!")
})
print("task is running...")
notify
메서드를 사용하면, 다른 스레드에서 동작하고 있는 작업이 끝난 경우, 설정해둔 작업을 실행할 수 있다.
let item = DispatchWorkItem(block: {
for _ in 0..<100 {
print("task")
}
})
item.cancel()
print(item.isCancelled) // true
DispatchQueue.global().async(execute: item) // 동작하지 않음
아직 작업이 수행되지 않은 경우 cancel
메서드를 사용하여 작업을 취소할 수 있다. 취소된 경우 스케쥴링 되어도 동작하지 않는다.
하나 이상의 작업 실행이 완료될 때까지 스레드를 차단하는 방법
한 화면을 구성하기 위해 여러 API 호출, Model로 파싱해야 하는 경우, 하나의 작업 그룹으로 묶는 것이 좋다. 이런 경우 사용하면 좋다. 여러 작업은 그룹에 연결하고, 비동기 실행을 위해서 예약하는 방식으로 사용할 수 있다. 모든 작업이 끝난 후, Completion Handler를 통해 완료 동작을 실행할 수 있다.
let group = DispatchGroup()
group.enter()
group.leave()
여러 작업이 있을 때, 시작하는 시점에서 enter, 작업이 끝났을 때 leave를 해주게 되면, enter 아래의 코드 블럭을 하나로 묶을 수 있다. 한 쌍으로 움직인다. enter할 경우 들어간 task count +1하고 leave할 때 -1 한다.
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
for _ in 0..<100 {
// some code
}
print("task 1 is done")
group.leave()
}
group.enter()
DispatchQueue.global().async {
for _ in 0..<100 {
// some code
}
print("task 2 is done")
group.leave()
}
group.wait()
print("all tasks are done")
dispatchGroup에 wait를 적어주게 되면, enter로 들어간 task들의 작업이 모두 끝난 시점에 다음 라인으로 넘어간다.
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
for _ in 0..<100 {
// some code
}
print("task 1 is done")
group.leave()
}
group.enter()
DispatchQueue.global().async {
for _ in 0..<100 {
// some code
}
print("task 2 is done")
group.leave()
}
group.notify(queue: .main, execute: {
print("All tasks are done.")
})
print("passed the notify code line.)
notify를 사용하게 되면, enter로 들어간 task들의 결과가 다 작동할 때 코드 블락이 실행되나, wait처럼 동기적으로 대기하지 않는다. 비동기로 코드 블락이 실행된다.
let group = DispatchGroup()
DispatchQueue.global().async(group: group, execute: {
for _ in 0..<100 {
// some code
}
print("task 1 is done")
})
DispatchQueue.global().async(group: group, execute: {
for _ in 0..<100 {
// some code
}
print("task 2 is done")
})
group.notify(queue: .main, execute: {
print("All tasks are done.")
})
print("passed the notify code line.)
파라미터에 group 자체를 넘김으로써 같은 효과를 낼 수 있다. 보다 코드가 깔끔하다.
시스템 이벤트를 비동기적으로 처리하기 위한 C기반 메커니즘
무슨 말일까..? 읽어서는 알 수가 없다.
let source = DispatchSource.makeTimerSource(queue: .main)
source.setEventHandler {
print("main queue에서 1초 뒤에 실행됨")
}
source.schedule(deadline: .now(), repeating: 1.0)
source.activate()
다음 글에서는 Operation Queue에 대해서 알아보도록 하자.