Operations
- Operation은 GCD와 비슷하게 동작합니다. GCD와 Operation 둘다 코드 덩어리를 분리된 thread에서 동작할수 있게 합니다. 그러나 Operation은 제출된 task를 훨씬 잘 제어할 수 있습니다. 앞에서 말했듯이 operation은 GCD를 기반으로 만들었습니다. 다른 operation에 대한 종속성, 실행중인 작업을 취소하는 기능, 좀더 복잡한 요구사항을 지원하는 객체지향 모델 같은 추가 기능을 지원합니다.
Reusability
- Operation을 사용하는 가장 첫번째 이유는 재사용성입니다. 간단히 실행 후 잊는 task는 GCD만 있으면 됩니다.
- Operation은 실제 Swift 객체입니다. 즉 입력을 전달하여 task를 설정하고 도움을 주는 메서드를 구현할 수 있습니다. 따라서 작업 단위 또는 테스크를 래핑하고 추후 언젠가 실행할 수 있습니다. 따라서 해당 작업단위를 여러번 제출할 수 있습니다.
Operation states
- Operation에는 라이프사이클을 대표하는 상태가 있습니다. 이 수명주기의 다양한 부분에서 발생할 수 있는 몇가지 상태가 있습니다.
- 인스턴스화되고 실행할 준비가 되면 isReady상태가 됩니다.
- 어떤 시점에 start매서드를 호출할 수 있으며, 이 대 isExecuting상태로 전환됩니다.
- 앱이 cancel메서드를 호출하면 isFinished상태로 전환되기 전에 isCancelled상태로 전환됩니다.
- cancel되지 않았다면 바로 isExecuting에서 isFinished로 전환됩니다.
BlockOperation
- BlockOperation class를 통해 빠르고 간단하게 Operation을 만들 수 있습니다.
let operation = BlockOperation {
print("2 + 3 = \(2 + 3)")
}
- BlockOperation은 default global queue에서 하나 이상의 closure들의 동시 실행을 관리합니다. 이미 OperationQueue를 사용하고 있고, 별도의 DispatchQueue를 사용하지 않으려는 앱을 위해 객체지향 래퍼를 제공합니다. Operation이기 때문에 KVO(Key-Value Observing) 알림, 종속성 및 operation이 제공하는 모든것을 사용할 수 있습니다.
- class 이름에서 바로 알 수 없는 것은 BlockOperation에서 closure그룹을 관리한다는 것입니다. dispatch group과 비슷하게 closure들이 모두 끝나면 종료된 것으로 표시합니다.
Note: BlockOperation에서 task는 concurrent하게 실행됩니다. 만약에 serial하게 실행해야한다면 private DispatchQueue에 제출하거나 종속성을 설정해야합니다.
Operation Queues
- OperationQueue는 GCD의 DispatchQueue처럼 operation의 scheduling과 동시에 최대사용할 수 있는 operation 갯수를 관리합니다.
- OperationQueue에는 3가지 사용 방법이 있습니다.
→ Operation, closure, operation 배열을 넘겨줌
OperationQueue management
- OperationQueue는 QoS 값과 operation이 가지고 있는 종속성에 따라 준비된 operation을 실행합니다. operation을 queue에 추가하면 종료되거나 취소될 때 까지 실행됩니다.
- Operation을 어떤 OperationQueue에 추가하면 같은 operation을 다른 OperationQueue에 추가할 수 없습니다. Operation인스턴스는 'Once and Done'한 task이므로 필요한 경우 여러번 실행하기 위해서는 서브클래스로 만들어야합니다.
Waiting for completion
- OperationQueue의 내부를 보면 waitUntilAllOperationsAreFinished이라는 메서드를 볼 수 있습니다. 이것은 정확이 이름대로 행동합니다. 해당 메서드를 호출하고 싶을때마다 wait란 단어를 block이라고 바꾸세요. 호출하면 현재 thread를 block하기 때문에 main UI thread에서 이 메서드를 호출하면 안됩니다.
- waitUntilAllOperationsAreFinished가 필요하다고 생각되면 이 blocking 메서드를 안전하게 호출할 수 있는 private serial DispatchQueue를 설정해야합니다. 모든 operation이 끝나는것을 기다릴 필요가 없고 일부 operation set만이라면 OperationQueue의 addOperations(_:waitUntilFinished:)를 대신 사용할 수 있습니다.
Quality of service
- OperationQueue는 DispatchGroup과 비슷하게 QoS가 다른 Operation을 추가할 수 있고 해당 우선순위에 따라 실행됩니다.
- default QoS는 .background입니다. operation queue에서 qualityOfService프로퍼티를 설정할 수 있지만 개별 operation의 QoS에 의해 QoS는 재정의 될 수 있습니다.
Pausing the queue
- isSuspended 프로퍼티를 true로 설정하여 dispatch queue를 일시중지할 수 있습니다. 이미 진행중인 operation들은 계속 실행될테지만 새롭게 추가된 operation들은 isSuspended가 false가 될 때까지 스케줄되지 않습니다.
Maximum number of operations
- 기본적으로 dispatch queue는 디바이스가 한번에 처리할 수 있는 수만큼의 작업을 실행합니다. 만약에 숫자를 제한하고 싶다면 단순히 maxConcurrentOperationCount 프로퍼티를 dispatch queue에 설정하면 됩니다. maxConcurrentOperationCount를 1로 설정하면 serial queue가 됩니다.
Underlying DispatchQueue
- OperationQueue에 operation을 추가하기 전에 존재하고 있는 DispatchQueue를 underlyingQueue으로 지정할 수 있습니다. 이 경우 dispatch queue의 QoS는 그 operation queue에 설정한 모든 QoS 값을 재정의합니다.
Note: main queue를 underlying queue로 사용하지 마세요!
Asynchronous Operations
-
이 때까지의 작업은 synchronous했으며 이는 Operation class의 상태와 잘 작동합니다. operation이 isReady상태가 되면 시스템은 사용가능한 thread를 검색하기 시작한다는 것을 알 수 있습니다. 스케줄러가 Operation을 실행할 thread를 찾으면 isExecuting 상태로 전환됩니다. 이 시점에 코드가 실행되고 완료되면 isFinished 상태가 됩니다.
-
비동기에서는 어떻게 작동할까요? operation의 main 메서드가 실행될 때 비동기 작업이 시작되고 그 후에 main 메서드를 종료합니다. 비동기 작업이 아직 완료되지 않았을 수 있기때문에 isFinished로 상태를 변경 할 수 없습니다
Asynchronous Operations
- Async 메서드를 operation으로 래핑할 수 있지만 사용자측에서 좀 더 작업이 필요합니다. task가 완료되면 operation이 자동으로 state를 설정할 수 없으므로 수동으로 관리해야합니다. 게다가 모든 state 프로퍼티는 읽기전용입니다.
- 사실 state관리는 간단합니다. 모든 Async operation의 기본이되는 class를 만들고 이것을 상속하면 다시 작업할 필요가 없습니다. 이것이 왜 framework의 일부가 아닌지 우린 모릅니다.
AsyncOperation
State tracking
- Operation의 state는 읽기전용이기 때무에 먼저 read-write방식으로 변경사항을 추적할 수 있는 방법을 제공해야합니다.
extension AsyncOperation {
enum State: String {
case ready, executing, finished
fileprivate var keyPath: String {
return "is\(rawValue.capitalized)"
}
}
}
- Operation class는 KVO notification을 사용합니다. 예를 들어 isExecuting state가 변하면 KVO notification이 전송됩니다. 그러나 직접 작성한 state는 'is'접두사로 시작하지 않으며 Swift 스타일 가이드에 따라 enum은 소문자여야합니다.
- computed property로 작성한 keyPath는 앞서 언급한 KVO notification을 지원하는데 도움이 됩니다. 현재 state에 keyPath를 요청하면 첫글자를 대문자로 바꾸고 접두어에 'is'를 추가합니다. 따라서 현재 상태가 'executing'이면 Operation base class의 프로퍼티와 일치하는 'isExecuting'을 반환합니다.
- fileprivate 접근제한자를 주의하세요. 이 파일에서는 keyPath를 사용할 수 있어야하나 외부에서 사용하면 안됩니다. 만약에 private으로 설정하면 enum 밖에서 보이지 않을 것입니다. 이제 범위 지정이 파일이므로 production 코드에 대해 class가 고유한 파일에 저장되야 합니다.
- 이제 state의 type을 만들었으니 state를 유지할 변수가 필요합니다. 값을 변경할 때 적절한 KVO notification을 보내야 하므로 프로퍼티에 옵저버를 붙입니다.
var state = State.ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
- 디폴트로 state는 ready입니다. state의 값을 변경하면 실제로 4개의 KVO notification을 보내게 됩니다. 현재 state가 ready이고 executing으로 업데이트되는 경우를 생각해보세요. isReady는 false로 바뀌고 isExecuting은 true가 될겁니다.
- 이 4개의 KVO notification은 전송됩니다.
- isReady를 위한 will change
- isExecuting을 위한 will change
- isReady를 위한 did change
- isExecuting을 위한 did change
- Operation Base class는 isExecuting과 isReady 프로퍼티 둘다 바뀌고 있다는 것을 알아야합니다.
Base properties
- 이제 state 변경을 추적하고 변경이 수행되었다는 것을 알리는 작업을 했으므로 base class의 메서드를 새로만든 state로 재정의 해야합니다.
override var isReady: Bool {
return super.isReady && state == .ready
}
override var isExecuting: Bool {
return state == .executing
}
override var isFinished: Bool {
return state == .finished
}
Note: 스케줄러가 operation이 사용할 thread를 찾을 준비가 되었는지 아닌지 여부를 결정하는 동안 코드가 진행되는 모든 것을 인식하지 못하므로 base class의 isReady 메서드를 체크하는 것을 포함하는 것이 중요합니다.
- 재정의할 마지막 프로퍼티는 단순히 Async Operation을 실제로 사용하고 있다는 것을 정의하는 것입니다.
override var isAsynchronous: Bool {
return true
}
Starting the operation
- 이제 남은 일은 start 메서드를 구현하는 것입니다. 수동으로 operation을 실행하던 operation queue이 자동으로 실행하던 start 메서드는 먼저 호출된 후에 main 을 호출합니다.
override func start() {
main()
state = .executing
}
Note: start를 재정의 할때는 super.start()를 호출해서는 안됩니다. 애플 공식문서(https://apple.co/2YcJvEh)에도 적혀있습니다.
- 위의 start 메서드의 2줄을 이상하게 생각할수도 있습니다. 비동기 task를 수행하고 있기 때문에 main 메서드가 거의 즉시 리턴됩니다. operation이 아직 진행 중임을 알 수 있게 수동으로 .executing state로 되돌려야 합니다.
출처: Concurrency by Tutorials Multithreading in Swift with GCD and Operations by Scott Grosch