본문 바로가기

학습 노트/iOS (2021)

172. Interoperation Dependencies

Operation간의 의존성 (Interoperation Dependencies)

Operation 사이의 의존성은 실행 순서를 결정한다.
의존성을 가지지 않은 Operation은 동시에 실행되고, 의존성을 가진 Operation은 이전 Operation이 완료되거나 취소된 경우 실행된다.

간단히 말하면 이전에 작성한 Complition Handeler를 통해 작동하는 Operation이 해당한다.

이들의 의존성은 단방향이며, 상호 의존은 불가능하고, 복수의 의존성을 가질 수는 있다.
이는 서로 다른 Operation Queue에 존재하더라도 유효하다.
Operation Class는 이를 위한 세가지 속성을 제공한다.

  • addDependency()
    의존성을 추가할 때 사용한다.
  • removeDependency()
    의존성을 제거할 때 사용한다.
  • dependencies
    Operation의 의존성을 확인할 때 사용한다.

의존성을 추가하고 제거하는 작업은 Queue에 등록되기 전에 진행되어야한다.

1. 20개의 이미지를 다운 받아 크기를 절반으로 축소한다.

2. Collection View에 이미지 목록을 표시한다.

3. 이미지에 필터를 적용하고, 완료된 이미지를 업데이트한다.

Download Operation > Reload Operation > Filter Operation > Reload Operation

의 순서로 의존성을 가진다.

//
//  DependencyViewController.swift
//  Concurrency Practice
//
//  Created by Martin.Q on 2021/12/22.
//

import UIKit

class DependencyViewController: UIViewController {
	
	let backgroundQueue = OperationQueue()
	let mainQueue = OperationQueue.main
	
	var uiOperation = [Operation]()
	var backgroundOperations = [Operation]()
	
	@IBOutlet weak var collectionView: UICollectionView!
	
	
	@IBAction func start(_ sender: Any) {
		PhotoDataSource.shared.reset()
		collectionView.reloadData()
		uiOperation.removeAll()
		backgroundOperations.removeAll()
	}
	
	@IBAction func cancel(_ sender: Any) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()

		PhotoDataSource.shared.reset()
    }

}

extension DependencyViewController: UICollectionViewDataSource {
	func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
		return PhotoDataSource.shared.list.count
	}
	func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
		let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
		let target = PhotoDataSource.shared.list[indexPath.item]
		if let imageView = cell.contentView.viewWithTag(100) as? UIImageView {
			imageView.image = target.data
		}
		
		return cell
	}
}
extension DependencyViewController: UICollectionViewDelegateFlowLayout {
	func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
		let w = collectionView.bounds.width / 3
		return CGSize(width: w, height: w * (768 / 1024))
	}
}

Collection View에 이미지를 표시하는 코드는 기존의 방식과 다르지 않다.

클래스 파일 내에는 Operation Queue와 Operation 배열이 선언돼있다.
Collection의 업데이트 작업은 Main Queue에, 나머지는 Background Queue에 추가한다.
Operation Queue에 Operation을 추가할 때는 의존성을 추가한 뒤 추가할 수 있도록 한다.
또한 Operation 배열의 Operation을 동시에 Queue에 추가할 수 있도록 한다.
이러한 방식이 의존성을 올바르게 적용하는 방식이다.

Scene의 버튼은 각각 작업을 시작하거나 취소할 수 있도록 구현한다.

@IBAction func start(_ sender: Any) {
	PhotoDataSource.shared.reset()
	collectionView.reloadData()
	uiOperation.removeAll()
	backgroundOperations.removeAll()
}

start Action은 실행시 Data 목록과 Operation 목록을 초기화 하도록 구현돼있다.
Action 메소드에서 구현한 코드는 Main Thread에서 실행된다.
Blocking 방지를 위해 Background Thread에서 실행되도록 구현한다.

@IBAction func start(_ sender: Any) {
	PhotoDataSource.shared.reset()
	collectionView.reloadData()
	uiOperation.removeAll()
	backgroundOperations.removeAll()
	
	DispatchQueue.global().async {
		let reloadOp = ReloadOperation(collectionView: self.collectionView)
		self.uiOperation.append(reloadOp)
		
		for (index, data) in PhotoDataSource.shared.list.enumerated() {
			let downloadOp = DownloadOperation(target: data)
			reloadOp.addDependency(downloadOp)
			self.backgroundOperations.append(downloadOp)
		}
	}
}

 

Collection 전체를 새로고침하는 Reload Operation을 생성하고 UiOperation 배열에 추가한다.

PhotoDataSource의 배열을 열거한후,
Download Operation을 생성하고, 초기화한다.
이렇게 되면 Download Operation이 완료된 다음 Reload Operation이 실행된다.

이후 background Operation에 Download Operation을 추가한다.

@IBAction func start(_ sender: Any) {
	PhotoDataSource.shared.reset()
	collectionView.reloadData()
	uiOperation.removeAll()
	backgroundOperations.removeAll()
	
	DispatchQueue.global().async {
		let reloadOp = ReloadOperation(collectionView: self.collectionView)
		self.uiOperation.append(reloadOp)
		
		for (index, data) in PhotoDataSource.shared.list.enumerated() {
			let downloadOp = DownloadOperation(target: data)
			reloadOp.addDependency(downloadOp)
			self.backgroundOperations.append(downloadOp)
			
			let filterOp = FilterOperation(target: data)
			filterOp.addDependency(reloadOp)
			self.backgroundOperations.append(filterOp)
			
			let reloadItemOp = ReloadOperation(collectionView: self.collectionView, indexPath: IndexPath(item: index, section: 0))
			reloadItemOp.addDependency(filterOp)
			self.uiOperation.append(reloadItemOp)
		}
	}
}

같은 방식으로 나머지 Operation들도 의존성에 맞게 추가한다.

@IBAction func start(_ sender: Any) {
	PhotoDataSource.shared.reset()
	collectionView.reloadData()
	uiOperation.removeAll()
	backgroundOperations.removeAll()
	
	DispatchQueue.global().async {
		let reloadOp = ReloadOperation(collectionView: self.collectionView)
		self.uiOperation.append(reloadOp)
		
		for (index, data) in PhotoDataSource.shared.list.enumerated() {
			let downloadOp = DownloadOperation(target: data)
			reloadOp.addDependency(downloadOp)
			self.backgroundOperations.append(downloadOp)
			
			let filterOp = FilterOperation(target: data)
			filterOp.addDependency(reloadOp)
			self.backgroundOperations.append(filterOp)
			
			let reloadItemOp = ReloadOperation(collectionView: self.collectionView, indexPath: IndexPath(item: index, section: 0))
			reloadItemOp.addDependency(filterOp)
			self.uiOperation.append(reloadItemOp)
		}
		
		self.backgroundQueue.addOperations(self.backgroundOperations, waitUntilFinished: false)
		self.mainQueue.addOperations(self.uiOperation, waitUntilFinished: false)
	}
}

이제 완성된 배열을 Queue에 전달한다.
이때 waitUntilFinished 속성은 false로 전달한다.
true로 전달하면 모든 Operation이 완료 될 때 까지 반환하지 않는다.
Background Queue에 추가할 때는 true로 전달해도 상관 없지만, Main Queue에서 전달하면 Main Thread가 Blocking된다.
따라서 해당 속성을 변경하려면 Main Main Queue에서 동작하는 Operation인지 주의가 필요하다.

@IBAction func cancel(_ sender: Any) {
	mainQueue.cancelAllOperations()
	backgroundQueue.cancelAllOperations()
}

cancel Action은 모든 Queue에서 cancelAllOperations 메서드를 호출해 모든 Operation을 취소하도록 구현한다.

//
//  PhotoDataSource.swift
//  Concurrency Practice
//
//  Created by Martin.Q on 2021/12/22.
//

import UIKit

class PhotoData {
	var url: URL
	var data: UIImage?
	
	init(url: String) {
		self.url = URL(string: url)!
	}
}

class PhotoDataSource {
	static let shared = PhotoDataSource()
	var list = [PhotoData]()
	let filterContext = CIContext(options: nil)
	
	func reset() {
		autoreleasepool {
			list.removeAll(keepingCapacity: true)
			
			(1...20).forEach {
				let url = "https://kxcodingblob.blob.core.windows.net/mastering-ios/\($0).jpg"
				let data = PhotoData(url: url)
				list.append(data)
			}
		}
	}
}

Coolection View에 표시되는 각각의 데이터는 위의 PhotoData 클래스로 구현돼있다.
전체 데이처는 PhotoDataSource 클래스로 저장된다.

//
//  DownloadOperation.swift
//  Concurrency Practice
//
//  Created by Martin.Q on 2021/12/22.
//

import UIKit

class DownloadOperation: Operation {
	let target: PhotoData
	
	init(target: PhotoData) {
		self.target = target
		
		super.init()
	}
	
	override func main() {
		autoreleasepool {
			print(self, "Start")
			
			defer {
				if isCancelled {
					print(self, "Cancelled")
				} else {
					print(self, "Done")
				}
			}
			
			guard !Thread.isMainThread else { fatalError() }
			guard !isCancelled else { print(self, "Cancelled"); return }
			
			do {
				let data = try Data(contentsOf: target.url)
				
				guard !isCancelled else { print(self, "Cancelled"); return}
				
				if let image = UIImage(data: data) {
					let size = image.size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
					UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
					let frame = CGRect(origin: CGPoint.zero, size: size)
					image.draw(in: frame)
					let resultImage = UIGraphicsGetImageFromCurrentImageContext()
					UIGraphicsEndImageContext()
					
					guard !isCancelled else { print(self, "Cancelled"); return }
					
					target.data = resultImage
				}
			} catch {
				print(error.localizedDescription)
			}
			
			Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(4)))
		}
	}
	
	override func cancel() {
		super.cancel()
		
		print(self, "Cancel")
	}
}

DownloadOperation 클래스에는 이미지를 다운로드하고 리사이징 하는 역할을 담당한다.
main 메소드의 마지막에는 결과를 확인하기 쉽도록 최대 4초의 지연이 추가돼있다.
cancel 메소드에는 호출 시점을 확인할 수 있도록 로그를 남기도록 구현돼있다.

//
//  ReloadOperation.swift
//  Concurrency Practice
//
//  Created by Martin.Q on 2021/12/23.
//

import UIKit

class ReloadOperation: Operation {
	weak var collectionView: UICollectionView!
	var indexPath: IndexPath?
	
	init(collectionView: UICollectionView, indexPath: IndexPath? = nil) {
		self.collectionView = collectionView
		self.indexPath = indexPath
		
		super.init()
	}
	
	override func main() {
		autoreleasepool {
			print(self, "Start", indexPath)
			
			defer {
				if isCancelled {
					print(self, "Cancelled", indexPath)
				} else {
					print(self, "Done", indexPath)
				}
			}
			guard Thread.isMainThread else { fatalError() }
			guard isCancelled else { print(self, "Cancelled"); return }
			
			if let indexPath = indexPath {
				if collectionView.indexPathsForVisibleItems.contains(indexPath) {
					collectionView.reloadItems(at: [indexPath])
				}
			} else {
				collectionView.reloadData()
			}
		}
	}
	
	override func cancel() {
		super.cancel()
		
		print(self, "Cancel")
	}
}

ReloadOperation 클래스에는 Collection을 새로고침하는 코드가 구현돼있다.
indexPath가 존재하면 해당 셀을 새로고침하고, 이외에는 Collection View 전체를 새로고침한다.

//
//  FilterOperation.swift
//  Concurrency Practice
//
//  Created by Martin.Q on 2021/12/23.
//

import UIKit

class FilterOperation: Operation {
	let target: PhotoData
	
	init(target: PhotoData) {
		self.target = target
		
		super .init()
	}
	
	override func main() {
		autoreleasepool {
			print(self, "Start")
			
			defer {
				if isCancelled {
					print(self, "Cancelled")
				} else {
					print(self, "Done")
				}
			}
			
			guard !Thread.isMainThread else { fatalError() }
			guard !isCancelled else { print(self, "Cancelled"); return }
			
			guard let source = target.data?.cgImage else { fatalError() }
			let ciImage = CIImage(cgImage: source)
			
			guard !isCancelled else { print(self, "Cancelled"); return }
			
			let filter = CIFilter(name: "CIPhotoEffectNoir")
			filter?.setValue(ciImage, forKey: kCIInputImageKey)
			
			guard !isCancelled else { print(self, "Cancelled"); return }
			
			guard let ciResult = filter?.value(forKey: kCIOutputImageKey) as? CIImage else { fatalError() }
			
			guard !isCancelled else { print(self, "Cancelled"); return }
			
			Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(3)))
			
			guard !isCancelled else { print(self, "Cancelled"); return }
			guard let cgImg = PhotoDataSource.shared.filterContext.createCGImage(ciResult, from: ciResult.extent) else {
			   fatalError()
			}
			target.data = UIImage(cgImage: cgImg)
		}
	}
	
	override func cancel() {
		super.cancel()
		
		print(self, "Cancel")
	}
}

Filter Operation에는 이미지에 필터를 적용한다.
마찬가지로 최대 3초의 무작위 지연시간을 추가해 결과를 확인할 수 있도록 돼있다.

완성된 코드의 실행 순서는 다음과 같다.

  • Download Operation 20개가 먼저 실행된다.
<Concurrency_Practice.DownloadOperation: 0x2814e8000> Start
<Concurrency_Practice.DownloadOperation: 0x2814ea400> Start
<Concurrency_Practice.DownloadOperation: 0x2814e8e00> Start
<Concurrency_Practice.DownloadOperation: 0x2814e9c00> Start
<Concurrency_Practice.DownloadOperation: 0x2814ea900> Start
<Concurrency_Practice.DownloadOperation: 0x2814ea000> Start
<Concurrency_Practice.DownloadOperation: 0x2814ea200> Start
<Concurrency_Practice.DownloadOperation: 0x2814e9a00> Start
<Concurrency_Practice.DownloadOperation: 0x2814ea600> Start
<Concurrency_Practice.DownloadOperation: 0x2814e9e00> Start
<Concurrency_Practice.DownloadOperation: 0x2814eab00> Start
<Concurrency_Practice.DownloadOperation: 0x2814eaf00> Start
<Concurrency_Practice.DownloadOperation: 0x2814ead00> Start
<Concurrency_Practice.DownloadOperation: 0x2814eb200> Start
<Concurrency_Practice.DownloadOperation: 0x2814eb400> Start
<Concurrency_Practice.DownloadOperation: 0x2814eb600> Start
<Concurrency_Practice.DownloadOperation: 0x2814eb800> Start
<Concurrency_Practice.DownloadOperation: 0x2814ea800> Start
<Concurrency_Practice.DownloadOperation: 0x2814ebb00> Start
<Concurrency_Practice.DownloadOperation: 0x2814ebd00> Start
<Concurrency_Practice.DownloadOperation: 0x2814ebb00> Done
<Concurrency_Practice.DownloadOperation: 0x2814eaf00> Done
<Concurrency_Practice.DownloadOperation: 0x2814eb600> Done
<Concurrency_Practice.DownloadOperation: 0x2814eab00> Done
<Concurrency_Practice.DownloadOperation: 0x2814e9c00> Done
<Concurrency_Practice.DownloadOperation: 0x2814ea000> Done
<Concurrency_Practice.DownloadOperation: 0x2814ea600> Done
<Concurrency_Practice.DownloadOperation: 0x2814ea900> Done
<Concurrency_Practice.DownloadOperation: 0x2814eb800> Done
<Concurrency_Practice.DownloadOperation: 0x2814eb200> Done
<Concurrency_Practice.DownloadOperation: 0x2814ead00> Done
<Concurrency_Practice.DownloadOperation: 0x2814ea200> Done
<Concurrency_Practice.DownloadOperation: 0x2814ea400> Done
<Concurrency_Practice.DownloadOperation: 0x2814e8000> Done
<Concurrency_Practice.DownloadOperation: 0x2814e9a00> Done
<Concurrency_Practice.DownloadOperation: 0x2814e8e00> Done
<Concurrency_Practice.DownloadOperation: 0x2814ea800> Done
<Concurrency_Practice.DownloadOperation: 0x2814e9e00> Done
<Concurrency_Practice.DownloadOperation: 0x2814ebd00> Done
<Concurrency_Practice.DownloadOperation: 0x2814eb400> Done
  • Collection View를 새로고침하는 Operation이 실행된다.
<Concurrency_Practice.ReloadOperation: 0x106c04b00> Start nil
<Concurrency_Practice.ReloadOperation: 0x106c04b00> Done nil
  • ReloadOperation이 완료되면, 의존성에 따라 FilterOperation이 실행된다.
<Concurrency_Practice.FilterOperation: 0x2814eb300> Start
<Concurrency_Practice.FilterOperation: 0x2814ea700> Start
<Concurrency_Practice.FilterOperation: 0x2814ebe00> Start
<Concurrency_Practice.FilterOperation: 0x2814e9900> Start
<Concurrency_Practice.FilterOperation: 0x2814ea500> Start
<Concurrency_Practice.FilterOperation: 0x2814e9f00> Start
<Concurrency_Practice.FilterOperation: 0x2814ea100> Start
<Concurrency_Practice.FilterOperation: 0x2814ea300> Start
<Concurrency_Practice.FilterOperation: 0x2814eb700> Start
<Concurrency_Practice.FilterOperation: 0x2814eac00> Start
<Concurrency_Practice.FilterOperation: 0x2814eb900> Start
<Concurrency_Practice.FilterOperation: 0x2814eb500> Start
<Concurrency_Practice.FilterOperation: 0x2814eaa00> Start
<Concurrency_Practice.FilterOperation: 0x2814e9b00> Start
<Concurrency_Practice.FilterOperation: 0x2814ebc00> Start
<Concurrency_Practice.FilterOperation: 0x2814eb100> Start
<Concurrency_Practice.FilterOperation: 0x2814eba00> Start
<Concurrency_Practice.FilterOperation: 0x2814eae00> Start
<Concurrency_Practice.FilterOperation: 0x2814e9d00> Start
<Concurrency_Practice.FilterOperation: 0x2814e9500> Start
<Concurrency_Practice.FilterOperation: 0x2814eac00> Done
  • 개별 Cell을 새로고침하는 ReloadOperation이 실행된다.
<Concurrency_Practice.ReloadOperation: 0x10560b290> Start Optional([0, 10])
<Concurrency_Practice.ReloadOperation: 0x10560b290> Done Optional([0, 10])
<Concurrency_Practice.FilterOperation: 0x2814e9900> Done
<Concurrency_Practice.ReloadOperation: 0x1056043a0> Start Optional([0, 1])
<Concurrency_Practice.ReloadOperation: 0x1056043a0> Done Optional([0, 1])
<Concurrency_Practice.FilterOperation: 0x2814ebc00> Done
<Concurrency_Practice.ReloadOperation: 0x105610830> Start Optional([0, 18])
<Concurrency_Practice.ReloadOperation: 0x105610830> Done Optional([0, 18])
<Concurrency_Practice.FilterOperation: 0x2814e9500> Done
<Concurrency_Practice.ReloadOperation: 0x10560bd90> Start Optional([0, 0])
<Concurrency_Practice.ReloadOperation: 0x10560bd90> Done Optional([0, 0])
<Concurrency_Practice.FilterOperation: 0x2814eaa00> Done
<Concurrency_Practice.ReloadOperation: 0x10560e330> Start Optional([0, 9])
<Concurrency_Practice.ReloadOperation: 0x10560e330> Done Optional([0, 9])
<Concurrency_Practice.FilterOperation: 0x2814ebe00> Done
<Concurrency_Practice.ReloadOperation: 0x105610950> Start Optional([0, 19])
<Concurrency_Practice.ReloadOperation: 0x105610950> Done Optional([0, 19])
<Concurrency_Practice.FilterOperation: 0x2814ea700> Done
<Concurrency_Practice.ReloadOperation: 0x10560e210> Start Optional([0, 8])
<Concurrency_Practice.ReloadOperation: 0x10560e210> Done Optional([0, 8])
<Concurrency_Practice.FilterOperation: 0x2814eba00> Done
<Concurrency_Practice.ReloadOperation: 0x105610710> Start Optional([0, 17])
<Concurrency_Practice.ReloadOperation: 0x105610710> Done Optional([0, 17])
<Concurrency_Practice.FilterOperation: 0x2814eb700> Done
<Concurrency_Practice.ReloadOperation: 0x1056100b0> Start Optional([0, 15])
<Concurrency_Practice.ReloadOperation: 0x1056100b0> Done Optional([0, 15])
<Concurrency_Practice.FilterOperation: 0x2814e9f00> Done
<Concurrency_Practice.ReloadOperation: 0x105609a10> Start Optional([0, 4])
<Concurrency_Practice.ReloadOperation: 0x105609a10> Done Optional([0, 4])
<Concurrency_Practice.FilterOperation: 0x2814ea300> Done
<Concurrency_Practice.ReloadOperation: 0x10560deb0> Start Optional([0, 6])
<Concurrency_Practice.ReloadOperation: 0x10560deb0> Done Optional([0, 6])
<Concurrency_Practice.FilterOperation: 0x2814eb900> Done
<Concurrency_Practice.ReloadOperation: 0x10560e0f0> Start Optional([0, 16])
<Concurrency_Practice.ReloadOperation: 0x10560e0f0> Done Optional([0, 16])
<Concurrency_Practice.FilterOperation: 0x2814e9d00> Done
<Concurrency_Practice.ReloadOperation: 0x10560e7a0> Start Optional([0, 3])
<Concurrency_Practice.ReloadOperation: 0x10560e7a0> Done Optional([0, 3])
<Concurrency_Practice.FilterOperation: 0x2814eb100> Done
<Concurrency_Practice.ReloadOperation: 0x10560b4d0> Start Optional([0, 12])
<Concurrency_Practice.ReloadOperation: 0x10560b4d0> Done Optional([0, 12])
<Concurrency_Practice.FilterOperation: 0x2814eae00> Done
<Concurrency_Practice.ReloadOperation: 0x10560b3b0> Start Optional([0, 11])
<Concurrency_Practice.ReloadOperation: 0x10560b3b0> Done Optional([0, 11])
<Concurrency_Practice.FilterOperation: 0x2814ea500> Done
<Concurrency_Practice.ReloadOperation: 0x10560dfd0> Start Optional([0, 7])
<Concurrency_Practice.ReloadOperation: 0x10560dfd0> Done Optional([0, 7])
<Concurrency_Practice.FilterOperation: 0x2814ea100> Done
<Concurrency_Practice.ReloadOperation: 0x105609b30> Start Optional([0, 5])
<Concurrency_Practice.ReloadOperation: 0x105609b30> Done Optional([0, 5])
<Concurrency_Practice.FilterOperation: 0x2814eb300> Done
<Concurrency_Practice.ReloadOperation: 0x10560b5f0> Start Optional([0, 13])
<Concurrency_Practice.ReloadOperation: 0x10560b5f0> Done Optional([0, 13])
<Concurrency_Practice.FilterOperation: 0x2814eb500> Done
<Concurrency_Practice.ReloadOperation: 0x10560b710> Start Optional([0, 14])
<Concurrency_Practice.ReloadOperation: 0x10560b710> Done Optional([0, 14])
<Concurrency_Practice.FilterOperation: 0x2814e9b00> Done
<Concurrency_Practice.ReloadOperation: 0x10560f930> Start Optional([0, 2])
<Concurrency_Practice.ReloadOperation: 0x10560f930> Done Optional([0, 2])

ReloadOperation은 다른 Operation들과 달리 main Queue에 추가돼있다.
Main Queue는 Serial Queue로서 이저넹 추가된 Operation이 완료돼야 다음 Operation이 실행된다.
따라서 ReloadOperation이 실행 될 때 마다 Done이 출력되고 다음 Operation이 실행된다.

<Concurrency_Practice.DownloadOperation: 0x2814f9100> Start
<Concurrency_Practice.DownloadOperation: 0x2814f8100> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9200> Start
<Concurrency_Practice.DownloadOperation: 0x2814f8300> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9400> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9600> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9800> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9a00> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9c00> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9f00> Start
<Concurrency_Practice.DownloadOperation: 0x2814fa100> Start
<Concurrency_Practice.DownloadOperation: 0x2814fa300> Start
<Concurrency_Practice.DownloadOperation: 0x2814fa500> Start
<Concurrency_Practice.DownloadOperation: 0x2814fa800> Start
<Concurrency_Practice.DownloadOperation: 0x2814faa00> Start
<Concurrency_Practice.DownloadOperation: 0x2814fac00> Start
<Concurrency_Practice.DownloadOperation: 0x2814fae00> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9e00> Start
<Concurrency_Practice.DownloadOperation: 0x2814fb100> Start
<Concurrency_Practice.DownloadOperation: 0x2814fb300> Start
<Concurrency_Practice.DownloadOperation: 0x2814f9800> Done
<Concurrency_Practice.DownloadOperation: 0x2814f9600> Done
<Concurrency_Practice.DownloadOperation: 0x2814fb100> Done
<Concurrency_Practice.DownloadOperation: 0x2814f8100> Done
<Concurrency_Practice.DownloadOperation: 0x2814f9100> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f8000> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f8200> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f8300> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f8a00> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f9200> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f9300> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f9400> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f9500> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f9700> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f9900> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f9a00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f9b00> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f9c00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814f9d00> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f9f00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fa000> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fa100> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fa200> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fa300> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fa400> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fa500> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fa700> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fa800> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fa900> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814faa00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fab00> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fac00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fad00> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fae00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814faf00> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814f9e00> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fb000> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fb200> Cancel
<Concurrency_Practice.DownloadOperation: 0x2814fb300> Cancel
<Concurrency_Practice.FilterOperation: 0x2814fb400> Cancel
<Concurrency_Practice.ReloadOperation: 0x106c08e20> Start Optional([0, 0])
<Concurrency_Practice.ReloadOperation: 0x106c08e20> Done Optional([0, 0])
<Concurrency_Practice.ReloadOperation: 0x106c0af30> Start Optional([0, 1])
<Concurrency_Practice.ReloadOperation: 0x106c0af30> Done Optional([0, 1])
<Concurrency_Practice.ReloadOperation: 0x106c04f40> Start Optional([0, 2])
<Concurrency_Practice.ReloadOperation: 0x106c04f40> Done Optional([0, 2])
<Concurrency_Practice.ReloadOperation: 0x106c05350> Start Optional([0, 3])
<Concurrency_Practice.ReloadOperation: 0x106c05350> Done Optional([0, 3])
<Concurrency_Practice.ReloadOperation: 0x106c05470> Start Optional([0, 4])
<Concurrency_Practice.ReloadOperation: 0x106c05470> Done Optional([0, 4])
<Concurrency_Practice.ReloadOperation: 0x106c07b30> Start Optional([0, 5])
<Concurrency_Practice.ReloadOperation: 0x106c07b30> Done Optional([0, 5])
<Concurrency_Practice.ReloadOperation: 0x106c07c50> Start Optional([0, 6])
<Concurrency_Practice.ReloadOperation: 0x106c07c50> Done Optional([0, 6])
<Concurrency_Practice.ReloadOperation: 0x106c07d70> Start Optional([0, 7])
<Concurrency_Practice.ReloadOperation: 0x106c07d70> Done Optional([0, 7])
<Concurrency_Practice.ReloadOperation: 0x106c04c20> Start Optional([0, 8])
<Concurrency_Practice.ReloadOperation: 0x106c04c20> Done Optional([0, 8])
<Concurrency_Practice.ReloadOperation: 0x106c04d40> Start Optional([0, 9])
<Concurrency_Practice.ReloadOperation: 0x106c04d40> Done Optional([0, 9])
<Concurrency_Practice.ReloadOperation: 0x106c07650> Start Optional([0, 10])
<Concurrency_Practice.ReloadOperation: 0x106c07650> Done Optional([0, 10])
<Concurrency_Practice.ReloadOperation: 0x106c07770> Start Optional([0, 11])
<Concurrency_Practice.ReloadOperation: 0x106c07770> Done Optional([0, 11])
<Concurrency_Practice.ReloadOperation: 0x106c07890> Start Optional([0, 12])
<Concurrency_Practice.ReloadOperation: 0x106c07890> Done Optional([0, 12])
<Concurrency_Practice.ReloadOperation: 0x106c06fc0> Start Optional([0, 13])
<Concurrency_Practice.ReloadOperation: 0x106c06fc0> Done Optional([0, 13])
<Concurrency_Practice.ReloadOperation: 0x106c070e0> Start Optional([0, 14])
<Concurrency_Practice.ReloadOperation: 0x106c070e0> Done Optional([0, 14])
<Concurrency_Practice.ReloadOperation: 0x106c07200> Start Optional([0, 15])
<Concurrency_Practice.ReloadOperation: 0x106c07200> Done Optional([0, 15])
<Concurrency_Practice.ReloadOperation: 0x106c04b00> Start Optional([0, 16])
<Concurrency_Practice.ReloadOperation: 0x106c04b00> Done Optional([0, 16])
<Concurrency_Practice.ReloadOperation: 0x106c0db30> Start Optional([0, 17])
<Concurrency_Practice.ReloadOperation: 0x106c0db30> Done Optional([0, 17])
<Concurrency_Practice.ReloadOperation: 0x106c0dc50> Start Optional([0, 18])
<Concurrency_Practice.ReloadOperation: 0x106c0dc50> Done Optional([0, 18])
<Concurrency_Practice.ReloadOperation: 0x106c0dd70> Start Optional([0, 19])
<Concurrency_Practice.ReloadOperation: 0x106c0dd70> Done Optional([0, 19])
<Concurrency_Practice.DownloadOperation: 0x2814f9200> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f9a00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f9c00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fae00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f8300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fa500> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fa800> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f9f00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fb300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f9400> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fa100> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fa300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814fac00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814faa00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f9e00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x2814f9100> Cancelled
<Concurrency_Practice.ReloadOperation: 0x106c06c00> Start nil
<Concurrency_Practice.ReloadOperation: 0x106c06c00> Done nil

동작중 취소하게 되면 위와 같은 로그를 확인 할 수 있다.

취소하기 전 이미 완료된 DownloadOperation을 제외한 모든 DownloadOperation과 FilterOperation이 즉시 취소됐다.
이후에 개별 Cell을 새로고침하는 Operation이 실행되고, 정상적으로 완료된 것을 볼 수 있다.
그 아래에 나머지 DownloadOperation의 취소 목록이 보이고, ReloadOperation이 실행됐다가 완료된 것도 볼 수 있다.
즉 ReloadOperation 들에도 취소 기능이 존재하지만 정상적으로 진행되지 않았음을 확인할 수 있다.

ReloadOperation들은 Main Queue에 존재하고, 해당 Operation들을 취소할 때는 cancelAllOperations 메서드로는 불가능하다.
이러한 Operation들은 직접 Cancel 메서드를 호출해야 정상적으로 취소된다.

@IBAction func cancel(_ sender: Any) {
	//mainQueue.cancelAllOperations()
	uiOperation.forEach { $0.cancel() }
	backgroundQueue.cancelAllOperations()
}

따라서 위와 같이 각각의 Operation들이 직접 cancel 메서드를 호출하게 하면

2022-01-07 03:20:08.409343+0900 Concurrency Practice[16689:7848324] Metal API Validation Enabled
<Concurrency_Practice.DownloadOperation: 0x281580200> Start
<Concurrency_Practice.DownloadOperation: 0x28159c900> Start
<Concurrency_Practice.DownloadOperation: 0x28159ca00> Start
<Concurrency_Practice.DownloadOperation: 0x28159cd00> Start
<Concurrency_Practice.DownloadOperation: 0x28159cf00> Start
<Concurrency_Practice.DownloadOperation: 0x28159d100> Start
<Concurrency_Practice.DownloadOperation: 0x28159d300> Start
<Concurrency_Practice.DownloadOperation: 0x28159d500> Start
<Concurrency_Practice.DownloadOperation: 0x28159d700> Start
<Concurrency_Practice.DownloadOperation: 0x28159da00> Start
<Concurrency_Practice.DownloadOperation: 0x28159dc00> Start
<Concurrency_Practice.DownloadOperation: 0x28159de00> Start
<Concurrency_Practice.DownloadOperation: 0x28159e000> Start
<Concurrency_Practice.DownloadOperation: 0x28159e300> Start
<Concurrency_Practice.DownloadOperation: 0x28159e500> Start
<Concurrency_Practice.DownloadOperation: 0x28159e700> Start
<Concurrency_Practice.DownloadOperation: 0x28159e900> Start
<Concurrency_Practice.DownloadOperation: 0x28159d900> Start
<Concurrency_Practice.DownloadOperation: 0x28159ec00> Start
<Concurrency_Practice.DownloadOperation: 0x28159ee00> Start
<Concurrency_Practice.ReloadOperation: 0x13b8056d0> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970ade0> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970ca30> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970d000> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970b960> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970ba80> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970bba0> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970bcc0> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970bde0> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970c020> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970c140> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970c260> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970c380> Cancel
<Concurrency_Practice.ReloadOperation: 0x139708fe0> Cancel
<Concurrency_Practice.ReloadOperation: 0x139709100> Cancel
<Concurrency_Practice.ReloadOperation: 0x139709220> Cancel
<Concurrency_Practice.ReloadOperation: 0x139709340> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970bf00> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970d640> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970d760> Cancel
<Concurrency_Practice.ReloadOperation: 0x13970d880> Cancel
<Concurrency_Practice.DownloadOperation: 0x281580200> Cancel
<Concurrency_Practice.FilterOperation: 0x281580500> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159c900> Cancel
<Concurrency_Practice.FilterOperation: 0x28159c100> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159ca00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159cc00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159cd00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159ce00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159cf00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159d000> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159d100> Cancel
<Concurrency_Practice.FilterOperation: 0x28159d200> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159d300> Cancel
<Concurrency_Practice.FilterOperation: 0x28159d400> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159d500> Cancel
<Concurrency_Practice.FilterOperation: 0x28159d600> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159d700> Cancel
<Concurrency_Practice.FilterOperation: 0x28159d800> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159da00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159db00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159dc00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159dd00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159de00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159df00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159e000> Cancel
<Concurrency_Practice.FilterOperation: 0x28159e200> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159e300> Cancel
<Concurrency_Practice.FilterOperation: 0x28159e400> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159e500> Cancel
<Concurrency_Practice.FilterOperation: 0x28159e600> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159e700> Cancel
<Concurrency_Practice.FilterOperation: 0x28159e800> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159e900> Cancel
<Concurrency_Practice.FilterOperation: 0x28159ea00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159d900> Cancel
<Concurrency_Practice.FilterOperation: 0x28159eb00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159ec00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159ed00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159ee00> Cancel
<Concurrency_Practice.FilterOperation: 0x28159ef00> Cancel
<Concurrency_Practice.DownloadOperation: 0x28159d500> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d500> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159ec00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159ec00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e900> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e900> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159cd00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159cd00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d300> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159de00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159de00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e000> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e000> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d900> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d900> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159cf00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159cf00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159da00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159da00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d100> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d100> Cancelled
<Concurrency_Practice.DownloadOperation: 0x281580200> Cancelled
<Concurrency_Practice.DownloadOperation: 0x281580200> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159dc00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159dc00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e700> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e700> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159ca00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159ca00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d700> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159d700> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159ee00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159ee00> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159c900> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159c900> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e500> Cancelled
<Concurrency_Practice.DownloadOperation: 0x28159e500> Cancelled

빠짐없이 모든 Operation이 취소됐음을 확인할 수 있다.

Background Queue의 동시 실행 Operation 수 조절하기

직접 생성한 Operation Queue는 Operation을 동시에 실행한다.
이 수는 작동 환경에 따라 자동으로 조절된다.
Operation Queue에는 maxConcurrentOperationCount 속성이 선언돼어있다.

해당 속성을 통해 동시에 실행할 Operation의 수를 조절할 수 있다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	PhotoDataSource.shared.reset()
	
	backgroundQueue.maxConcurrentOperationCount = 1
}

즉, 이렇게 1로 설정하면 MainQueue와 같이 순서대로 하나씩 실행하게 된다.
ConcurrentQueue를 SerialQueue로 바꾸는 경우 자주 사용된다.
만약 5보다 작은 값으로 설정하게 되면 CPU 부하를 줄일 수 있기도 하다.