본문 바로가기

학습 노트/iOS (2021)

175. Dispatch Group, Dispatch Semaphore

Dispatch Group

Dispatch Queue는 추가된 작업들을 가사으이 그룹으로 관리한다.
이는 서로 다른 Queue에 추가된 작업을 동일한 그룹으로 관리하는 것도 가능하다.
이름 그대로 여러 작업을 하나로 묶는 개념이다.
따라서 묶인 모든 작업이 완료돼야 그룹이 완료된다.

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

import UIKit

class DispatchGroupViewController: UIViewController {
	
	let workQueue = DispatchQueue(label: "WorkQueue", attributes: .concurrent)
	let serialWorkQueue = DispatchQueue(label: "SerialWorkQueue")
	
	@IBAction func submit(_ sender: Any) {
		workQueue.async {
			for _ in 0..<10 {
				print("🍏", separator: "", terminator: "")
				Thread.sleep(forTimeInterval: 0.1)
			}
		}
		
		workQueue.async {
			for _ in 0..<10 {
				print("🍎", separator: "", terminator: "")
				Thread.sleep(forTimeInterval: 0.2)
			}
		}
		
		workQueue.async {
			for _ in 0..<10 {
				print("🍋", separator: "", terminator: "")
				Thread.sleep(forTimeInterval: 0.3)
			}
		}
	}
}

workQueue는 ConcurrentQueue이고, serialQueue는 SerialQueue이다.
메소드에는 각각 다른 지연시간으로 과일을 출력하는 코드가 작성돼있다.
모든 작업을 하나의 그룹으로 묶고 결과를 확인해 본다.

기존의 방식

@IBAction func submit(_ sender: Any) {
	group.enter()
	workQueue.async {
		for _ in 0..<10 {
			print("🍏", separator: "", terminator: "")
			Thread.sleep(forTimeInterval: 0.1)
		}
		self.group.leave()
	}
	
	workQueue.async {
		for _ in 0..<10 {
			print("🍎", separator: "", terminator: "")
			Thread.sleep(forTimeInterval: 0.2)
		}
	}
	
	workQueue.async {
		for _ in 0..<10 {
			print("🍋", separator: "", terminator: "")
			Thread.sleep(forTimeInterval: 0.3)
		}
	}
}

기존의 방식은 enter 메소드를 호출하고, 작업이 완료 되면 leave 메소드를 호출해 알렸다.
이 경우 둘이 한 쌍을 이뤄야 하기 때문에 실수가 낮아 새로운 방식이 도입됐다.

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

import UIKit

class DispatchGroupViewController: UIViewController {
	
	let workQueue = DispatchQueue(label: "WorkQueue", attributes: .concurrent)
	let serialWorkQueue = DispatchQueue(label: "SerialWorkQueue")
	
	let group = DispatchGroup()
	
	@IBAction func submit(_ sender: Any) {
		workQueue.async(group: group) {
			for _ in 0..<10 {
				print("🍏", separator: "", terminator: "")
				Thread.sleep(forTimeInterval: 0.1)
			}
		}
		
		workQueue.async(group: group) {
			for _ in 0..<10 {
				print("🍎", separator: "", terminator: "")
				Thread.sleep(forTimeInterval: 0.2)
			}
		}
		
		workQueue.async(group: group) {
			for _ in 0..<10 {
				print("🍋", separator: "", terminator: "")
				Thread.sleep(forTimeInterval: 0.3)
			}
		}
		group.notify(queue: DispatchQueue.main) {
			print("Done")
		}
	}
}

새로운 방식에는 async의 오버로딩 버전을 사용한다.
async의 파라미터로 gorup을 전달하면 간단히 추가할 수 있다.

DispatchGroup 또한 WorkItem과 마찬가지로 notify, wait 메서드를 제공한다.
이걸 사용해서 완료 시점을 확인 할 수 잇다.

완료 시점은 위와 같다.
가장 늦은 레몬을 전부 출력하고 나서야 Done이 출력되낟.

 

Dispatch Semaphore

Dispatch Semaphore는 Counting Semaphore에 해당한다.
동기화와 서로 다른 Queue에 속한 작업의 실행 순서를 제어해 본다.

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

import UIKit

class DispatchSemaphoreViewController: UIViewController {
	var value = 0
	
	@IBOutlet weak var valueLabel: UILabel!
	
	let workQueue = DispatchQueue(label: "workQueue", attributes: .concurrent)
	let group = DispatchGroup()
	
	
	@IBAction func synchronize(_ sender: Any) {
		value = 0
		valueLabel.text = "\(value)"
		
		workQueue.async(group: group) {
			for _ in 1...1000 {
				self.value += 1
			}
		}
		
		workQueue.async(group: group) {
			for _ in 1...1000 {
				self.value += 1
			}
		}
		
		workQueue.async(group: group) {
			for _ in 1...1000 {
				self.value += 1
			}
		}
		
		group.notify(queue: DispatchQueue.main) {
			self.valueLabel.text = "\(self.value)"
		}
	}
	
	@IBAction func controlExecutionOrder(_ sender: Any) {
		value = 0
		valueLabel.text = "\(value)"
		
		workQueue.async {
			for _ in 1...1000 {
				self.value += 1
				Thread.sleep(forTimeInterval: 0.1)
			}
		}
		
		DispatchQueue.main.async {
			self.valueLabel.text = "\(self.value)"
		}
	}

}

workQueue로 추가되는 작업들은 모두 같은 group에 추가된다.
모든 작업은 value의 값을 1씩 증가시키며 각각 1000번 반복한다.
작업이 완료되면 value의 최종 값을 Label에 표시한다.

이론적으로는 3000이 돼야 할 것 같지만 다른 값이 업데이트 됐다.
또한 해당 값도 일정하지 않다.
모든 Queue가 Value에 동시에 접근하기 때문에 발생하는 문제이다.
값의 동기화 문제가 생기는 것이다.

따라서 복수의 작업에서 동시에 value에 접근하지 못하도록 해야한다.
Concurrent Queue를 Serial Queue로 바꾸는 것,
Semaphore를 사용하는 것이다.

let semaphore = DispatchSemaphore(value: 1)

이 경우 위와 같이 semoaphore를 생성할 수 있다.

작업은 semaphore에게 가능 여부를 요청한다.
semaphore는 자신의 Count가 0보다 큰지 확인하고, 크다면 Count를 감소시키고 작업을 허가한다.
Count가 0이라면 증가할 때 까지 대기한다.

@IBAction func synchronize(_ sender: Any) {
	value = 0
	valueLabel.text = "\(value)"
	
	let semaphore = DispatchSemaphore(value: 1)
	
	workQueue.async(group: group) {
		for _ in 1...1000 {
			semaphore.wait()
			self.value += 1
			semaphore.signal()
		}
	}
	
	workQueue.async(group: group) {
		for _ in 1...1000 {
			semaphore.wait()
			self.value += 1
			semaphore.signal()
		}
	}
	
	workQueue.async(group: group) {
		for _ in 1...1000 {
			semaphore.wait()
			self.value += 1
			semaphore.signal()
		}
	}
	
	group.notify(queue: DispatchQueue.main) {
		self.valueLabel.text = "\(self.value)"
	}
}

wait 메소드로 semaphore에 허가를 요청하고,
signal 메소드로 작업이 완료 됐음을 알린다.
이제는 모든 작업이 semaphore에 따라 동시에 value에 접근할 수 없게 된다.

실행순서 제어

@IBAction func controlExecutionOrder(_ sender: Any) {
	value = 0
	valueLabel.text = "\(value)"
	
	workQueue.async {
		for _ in 1...1000 {
			self.value += 1
			Thread.sleep(forTimeInterval: 0.1)
		}
	}
	
	DispatchQueue.main.async {
		self.valueLabel.text = "\(self.value)"
	}
}

controlExcutionOrder 메서드는 
workQueue에 추가되는 작업은 value를 증가시키고,
main Queue에 추가되는 작업은 해당 값을 Label에 업데이트한다.

서로 다른 Dispatch Queue에 추가되지 않기 때문에 순서대로 실행 되지 않는다.

@IBAction func controlExecutionOrder(_ sender: Any) {
	value = 0
	valueLabel.text = "\(value)"
	
	workQueue.async {
		for _ in 1...1000 {
			self.value += 1
			Thread.sleep(forTimeInterval: 0.1)
		}
	
		DispatchQueue.main.async {
			self.valueLabel.text = "\(self.value)"
		}
	}
}

이렇게 안에서 호출해도 되지만 semaphore를 사용해 순서를 지정할 수도 있다.

@IBAction func controlExecutionOrder(_ sender: Any) {
	value = 0
	valueLabel.text = "\(value)"
	
	let semaphore = DispatchSemaphore(value: 0)
	
	workQueue.async {
		for _ in 1...1000 {
			self.value += 1
			Thread.sleep(forTimeInterval: 0.1)
		}
	
		semaphore.signal()
	}
	
	DispatchQueue.main.async {
		semaphore.wait()
		self.valueLabel.text = "\(self.value)"
	}
}

Count가 0인 semaphore를 생성한다.
이후, workQueue의 작업의 끝에서 signal 메서드를 호출한다.
semaphore의 최대 Count가 0이므로 Count가 증가하지는 않지만,
작업 완료 됐음을 다른 Queue에 알려 줄 수 있다.
따라서 Lable을 업데이트 하는 Queue의 초입에서 wait 메서드를 호출한다.

결과적으로 Label을 업데이트하는 Main Queue에서 Wait 메서드를 호출했으므로,
점유상태가 유지돼 앱은 작업이 완료 되고 Label이 업데이트 되기까지 반응하지 않는다.

이러한 경우가 생기지 않도록 main Queue에서 wait을 호출하는 경우는 피해야 한다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

177. Data Persistence Overview  (0) 2022.01.10
176. GCD in Action  (0) 2022.01.10
174. Dispatch Work Item & Dispatch Source Timer  (0) 2022.01.09
173. GCD #1 (Grand Central Dispatch)  (0) 2022.01.07
172. Interoperation Dependencies  (0) 2022.01.07