이전에 학습했던 GCD / Operation의 심화 버전이며, 비동기 작업에 대한 강의를 해주시는 앨런의 강의를 듣고 기록합니다. 이해가 되지 않았던 부분을 한 번에 해소해준 정말 좋은 강의였습니다!
iOS Concurrency(동시성 프로그래밍)에 대한 이해
여러 가지 Task들이 있는데, 비슷한 Task끼리 그룹을 만들고, 해당 그룹이 언제 끝나는지 알고 싶어서!!! 그룹이라는 것을 만든다
이렇게 특정 그룹의 모든 작업 완료 시점을 notify
메서드를 통해서 알 수 있다!!
이런 경우가 있을까????
한 화면에서 동시에 여러 애니메이 효과가 겹쳐져 있을 때, 혹은 한꺼번에 많은 데이터를 받아올때!!
위 예시에서 앱의 첫화면의 각 Image들을 받아오는 작업들을 Group으로 전부 묶어서 해당 Group이 끝날 때 첫 화면으로 넘어갈 수 있게 해주면, 유저 입장에서는 로딩이 완벽히 되어진 화면을 처음 접할 수 있다!!
요약 : 디스패치그룹은 여러 개의 작업들이 비동기적으로 동시적으로 실행되고 있을때, 작업들을 "그룹화"해서 해당 작업들의 전체 종료시점을 알기 위한 개념이다!!
let group1 = DispatchGroup()
DispatchQueue.global(qos: ).async(group: group1) { 동기적 함수 }
DispatchQueue.global(qos: ).async(group: group1) { 동기적 함수 }
DispatchQueue.global().async(group: group1) { 동기적 함수 }
group1.notify(queue: DispatchQueue.main) { [weak self] in
self?.textLabel.text = "모든 작업이 완료되었습니다."
}
바로 위에서 설명할때는 group
을 지정해준 클로저에 동기적 함수를 넣어줬는데, 만약 아래 task1
에 비동기적 함수를 호출하게 되면?
그렇게 되면 group은 위와 같이 잘못된 시점을 group
이 끝난 시점으로 알게되는 현상이 일어나게 된다. 그렇다면 제대로 끝나는 시점을 알 수 있는 방법이 있을까?? 마치 reference count
를 세는 것처럼 입장
퇴장
개념을 사용할 수 있다!
이렇게 할 경우 입장
보다 퇴장
의 수가 더 적을 경우 group
의 수행이 끝나지 않았다고 판단하기 때문에 group
의 끝나는 시점을 알 수 있다
queue.async(group: group1) {
group1.enter() // 입장1
someAsyncMethod {
group1.leave() // 퇴장1
}
}
요약
1. 디스패치 그룹에서, 비동기적인 처리내의 클로저에서 "동기적인 함수"만 사용할때는 단순하게 클로저에 작업을 넣어서 사용하면 된다
2. 디스패치 그룹에서, 비도기적인 처리내의 클로저에서 "비동기적인 함수"를 사용할때는, DispatchQueue의 클로저에 비동기함수가 배치되는 순간 다시 다른 쓰레드로 비동기적으로 작업이 다시 보내질 수 있고(작업이 순차적으로 처리되지 않으므로), 이로인해 잘못된 작업 종료시점을 종료시점으로 인지할 수 있으므로 enter(), leave() 메서드를 사용해서 제대로된 종료시점을 관리해야 한다
Task라고 했던 것들을 Task 클래스를 상속함으로써 Operation 처럼 Task를 미리 객체화할 수 있다.
특징
1) 작업을 미리 정의해 놓고 사용하는, 큐에 제출하기 위한 객체
2) 빈약한 <취소 기능>을 내장
3) 빈약한 <순서 기능>을 내장
자 아래와 같이 일단 작업을 명세 시켜놓고 나중에 Queue에 넣어서 편하게 비동기 작업을 실행할 수 있다. 이렇게 되면 반복적으로 작업을 계속해서 Queue에 넣어줄 수도 있을 것이다.
cancel() 메서드 존재
let item1 = DispatchWorkItem(qos: .utility) {
print("Task 1 시작")
sleep(2)
print("Task 1 완료")
}
let item2 = DispatchWorkItem(qos: .utility) {
print("Task2 시작")
print("Task2 완료")
}
let queue = DispatchQueue.global()
queue.async(execute: item1)
queue.async(execute: item2)
item1.cancel()
//Task2 시작
//Task2 완료
위의 코드 처럼 cancel() 해서 작업이 시작 안된 작업을 제거 해준다
But, 이미 작업이 실행된 경우는 작업을 멈출 수 없다. cancel() 을 해줘도 멈춰지지 않는다.
그러나 일단은 isCancelled가 true값이 되었으므로 이 값을 통해서 다른 코드가 실행되지 않게 해준다
if item2.isCancelled {
//print("Task1 완료") 코드가 실행되지 않게하는 코드를 해당 영역에 넣어줌으로써 실행되지 않게 해줘야 한다
}
notify(queue: 실행할 큐, execute: 디스패치아이템)
메서드 존재
(직접적으로 실행 다음에, 실행할 아이템(작업)을 지정)
let item1 = DispatchWorkItem(qos: .utility) {
print("task1 : 출력하기")
print("task2 : 출력하기")
}
let item2 = DispatchWorkItem {
print("task3 : 출력하기")
print("task4 : 출력하기")
}
item1.notify(queue: DispatchQueue.global(), execute: item2)
queue.async(execute: item1)
요약
1. 디스패치워크아이템(DispatchWorkItem)은 작업을 간단하게 객체화하는 방법이다.
(뒤에서 배울 Operation의 간단한 버전이라고 생각하면 된다)
2. 작업을 객체화하고, 변수(상수)에 담아 간단하게 사용할 수 있다
3. 디스패치워크아이템을 이용하면, 오퍼레이션에 비해 빈약하지만, 작업을 취소하거나 / 순서화시키는 작업이 가능하다
Semaphore?? = 한국말로 "수기 신호"
공유 리소스에 접근가능한 작업 수를 제한해야할 경우
let semaphore = DispatchSemaphore(value: 3) // 한번에 실행하고자 하는 작업의 수를 지정할 수 있다
queue.async(group: group1) {
group1.enter()
semaphore.wait()
someAsyncMethod() {
group1.leave()
semaphore.signal()
}
}
일단 Task들을 실행하면 wait()
메서드를 통해서 기다리게 만든다.
그리고 작업 개수가 3개가 아니면 실행하게 하고 이미 실행되고 있는 Task들은 signal
메서드를 통해서 다른 Task들에게 한 자리가 비게 될 것을 알려준다.
아까 본 DispatchGroup의 enter()
, leave()
와 비슷한 과정이네??
근데 아까 group의 경우, group 내의 Task가 전부 끝나는 시점을 알기 위한 것이고
wait(), signal()의 경우는 최대 Task를 알 수 있기 위해서 그런 것이다
요약
세마포어(Semaphore)는 동시에 실행하는 작업의 갯수를 제한 가능한 방법이다.
최대 동시에 처리 가능한 작업의 갯수를 설정할 수 있고, 내부적인 메커니즘을 알아서 관리해준다.
우리는 항상 비동기 작업을 할 경우, 여러 쓰레드에서 공유 자원을 사용하여 일어나는 문제점에 대해서 항상 해결하기 위해 노력해야 된다.
위 사진 처럼 count
를 사용하게 되면 _count
를 여러 쓰레드에서 접근하게 되면서 문제가 생길 수 있다.
그래서 위 처럼 구현하지 말고, 아래처럼 적절하게serialQueue
의 sync()
메서드에 넣어줘야 된다.
seralQueue
인 것은 이해하겠지?? 공유자원에 접근할 때 무조건 serialQueue
에 넣어서 접근하게 되면 차례대로 접근할테니 공유자원을 같은 시점에 변경할 일도 없을 테니 말이야!
근데, 왜 async
가 아닌 sync
메서드를 쓸까??
일단 저 count가 DispatchQueue.global().async { }
클로저 내에 있다고 생각을 해보면 된다!
만약에 위 사진에 있는 sync
가 async
라면??DispatchQueue.global().async()
내부 클로저는 동기적으로 작용되어야 되는데, 비동기적으로 작동하면 바로 return 되기 때문에 문제가 생긴다
요약
1. 하나의 객체를 여러 개의 쓰레드에서 접근한다면, Thread-safe처리가 필요할 수 있다.
2. Thread-safe를 위한 시리얼큐 처리, 그리고 읽기 작업을 고려한 sync메서드를 적절하게 고려해서 설계해야 한다.(메인 쓰레드가 아닌, 큐에서 sync메서드를 호출하는 것은 가능하다.)
Operation 의 고유 기능으로 여러 가지가 있지만, 가장 중요한 것은 취소
, 순서지정(의존성)
이다
이것은 지난 시간에 했었다
순서지정을 자세히 배우지는 않았는데 간단하게 Dependency
설정을 통해서 하면 된다
프로퍼티를 이용해서 상태 체크를 하고 해당 상태를 통해서 순서지정에 사용한다
추상 클래스인 Operation 을 구체화 시켜서 사용해보자
Operation 은 크게 세 부분으로 나눌 수 있다!
이미지 다운로드를 하는 예제라고 생각해보자!!
maxConcurrentOperationCount
프로퍼티 를 통해서 몇개의 Opeation이 동시에 실행될 수 있는지 설정해줄 수 있다.
1일 경우에는 Serail
하게 동작할 것이다
Qos(서비스 품질) 설정은?
요약
1. 오퍼레이션큐는 내부적으로 GCD를 기반으로 구현되어 있다.
2. 오퍼레이션큐를 사용시 GCD방식으로 클로저를 이용해서 쉽게 사용할 수도 있지만, 오퍼레이션 객체를 오퍼레이션 큐에 넣어서 사용하면 여러가지 기능(순서지정 / 취소)를 사용할 수 있다.
추상 클래스 Operation 을 상속한 클래스를 만들어서 main()
메서드 내에서 비동기 함수를 내장할 때
아래와 같은 문제가 있을 수 있다
이 문제를 해결하기 위해서는 Operation
을 상속하는 AsyncOperation
을 정의해서 써야된다
class AsyncOperation: Operation {
// Enum 생성
enum State: String {
case ready, executing, finished
// KVO notifications을 위한 keyPath설정
fileprivate var keyPath: String {
return "is\(rawValue.capitalized)"
} // isReady/isExecuting/isFinished
}
// 직접 관리하기 위한 상태 변수 생성
var state = State.ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
}
extension AsyncOperation {
// 상태속성은 모두 read-only
override var isReady: Bool {
return super.isReady && state == .ready
}
override var isExecuting: Bool {
return state == .executing
}
override var isFinished: Bool {
return state == .finished
}
override var isAsynchronous: Bool { // 무조건 true로 리턴
return true
}
override func start() {
if isCancelled {
state = .finished
return
}
main()
state = .executing
}
override func cancel() {
super.cancel()
state = .finished
}
}
해당 AsyncOperation 코드는 직접 구현하기 보다는 가져다가 쓰자!!
위 AsyncOperation
을 상속받은 객체에 input
output
main()
을 구체화 시키고
main()
에서 직접 상태를 관리해줘야 한다!!
class SumOperation: AsyncOperation {
var lhs: Int
var rhs: Int
var result: Int?
// 초기화메서드 포함(속성 설정 때문에)
init(lhs: Int, rhs: Int) {
self.lhs = lhs
self.rhs = rhs
super.init()
}
override func main() {
asyncAdd_OpQ(lhs: lhs, rhs: rhs) { result in
self.result = result
self.state = .finished // 상태관리 수동 ⭐️
}
}
}
요약
1. 오퍼레이션의 main() 함수에서 비동기적인 작업이 이루어지는 경우, 잘못된 작업 종료 시점이 인식될 수 있으므로 그에 관련된 처리를 하는, 추상화 비동기 오퍼레이션 AsyncOperation을 구현해야 된다.
2. 필요성, 그리고 개념만 이해하고, "비동기 오퍼레이션" 정의 코드는 잘 복사해서 사용하면 된다.
3. 비동기 오퍼레이션(작업 객체)은 상태를 수동으로 관리할 수 있는 내용이 구현되어 있고, 비동기 오퍼레이션 main() 함수에서 self.state = .finished만 잘 설정해주면 된다.
cancel()
메서드를 이용해서 Operation 을 취소할 수도 있으며, cancelAllOperations()
를 통해서 OperationQueue 의 Operation 모두를 취소하기도 함
cancel
메서드를 실행하면 isCancelled
값이 true가 되는 것이다. 이것을 통해서 오퍼레이션이 동작하지 않도록 직접 구현해줘야 한다!
그리고 상태 값도 변경해줘야 한다!
요약
1. 오퍼레이션큐에는cancelAllOperations()
메서드가 있어, 오퍼레이션큐 내부에 있는 모든 오퍼레이션(작업)의 cancel() 메서드를 호출한다
2. 오퍼레이션의 cancel() 메서드는 실제 작업을 멈추는 것은 아니고, isCancelled = true로 설정한다
3. isCancelled 속성을 직접적으로 확인하는 코드를 심어서, 작업이 실제로 취소 되도록 구현한다
위 그림과 같이 다운로드 -> 압축풀기 -> 이미지 필터 -> 컴플리션 처리 하는 순서대로 작업되도록 종속성을 줘보자!!
위 처럼 의존성을 설정하고 프로토콜을 통해서 압축파일을 전달해보자
...
...
..
...
...
...
파일 참고
요약
1. 오퍼레이션(작업)에 의존성(Dependency) 설정을 해서, 순서를 지정할 수 있다.
2. 순서설정과 실제 데이터 전달이 필요한데, 데이터를 전달하는 프로토콜을 만들어, 데이터를 전달 할 수 있다.