본문 바로가기
iOS/System

Groups & Semaphores - Concurrency by Tutorials

by 탄이. 2020. 6. 14.

Chapter 4: Groups & Semaphores

Concurrency by Tutorials
In this part of the book, you're going to learn about the basics of Concurrency. You're going to learn what it is, what kind of problems it solves, and why would you even use it? Further, you will learn the basic pieces of which Concurrency comprises in Cocoa development: Grand Central Dispatch and Operations.
https://store.raywenderlich.com/products/concurrency-by-tutorials
  • 때로는 작업을 대기열에 넣는 대신 작업 그룹을 처리해야 하는 경우가 있습니다.
  • 모두 동시에 실행할 필요는 없지만 모두 완료한 시점을 알아야 합니다.
  • Apple은 이 정확한 시나리오를 위해 Dispatch Groups을 제공합니다.

DispatchGroup

  • 적절하게 이름이 지정된 DispatchGroup 클래스는 작업 그룹의 완료를 추적 할 때 사용하는 클래스입니다.
  • DispatchGroup을 초기화하여 시작하십시오. 하나가 있고 해당 그룹의 일부로 작업을 추적하려면 디스패치 큐의 비동기 메소드에 인수로 그룹을 제공 할 수 있습니다.
let group = DispatchGroup()

someQueue.async(group: group) { ... your work ... } 
someQueue.async(group: group) { ... more work .... } 
someOtherQueue.async(group: group) { ... other work ... } 

group.notify(queue: DispatchQueue.main) { [weak self] in 
	self?.textLabel.text = "All jobs have completed" 
}
  • 위의 예제 코드에서 볼 수 있듯이 그룹은 단일 디스패치 큐에 고정되지 않습니다.
  • 실행해야 하는 작업의 우선 순위에 따라 단일 그룹을 사용하면서 여러 대기열에 작업을 제출할 수 있습니다.
  • DispatchGroupnotify(queue:) 메소드를 제공하며, 제출 된 모든 작업이 완료 되자마자 통지 할 수 있습니다.
💡
notification 자체가 비동기적이기 때문에 기존에 제출했던 작업이 아직 완료되지 않은 한, notify(queue:)를 호출한 후 그룹에 더 많은 작업을 제출할 수 있습니다.
  • notify 메소드는 디스패치 큐를 매개 변수로 사용합니다.
  • 작업이 모두 완료되면 제공 한 마감이 표시된 발송 대기열에서 실행됩니다.
  • 표시되는 알림 호출은 가장 자주 사용하는 버전 일 수 있지만 서비스 품질()을 지정할 수있는 다른 버전도 있습니다.

Synchronous waiting

  • 어떤 이유로 그룹의 완료 알림에 비동기 적으로 응답 할 수 없는 경우 대신 디스패치 그룹에서 대기 메소드를 사용할 수 있습니다.
  • 모든 작업이 완료 될 때까지 현재 대기열을 차단하는 동기식 방법입니다.
  • 작업이 완료 될 때까지 대기하는 시간을 지정하는 선택적 매개 변수가 필요합니다.
  • 지정되지 않은 경우 무한 대기 시간이 있습니다.
let group = DispatchGroup()

someQueue.async(group: group) { ... } 
someQueue.async(group: group) { ... } 
someOtherQueue.async(group: group) { ... }

if group.wait(timeout: .now() + 60) == .timedOut { 
	print("The jobs didn’t finish in 60 seconds") 
}
💡
이것은 현재 스레드를 차단합니다. 절대 메인 큐에서 wait(timeout:)을 호출하지 마십시오.

Wrapping asynchronous methods

queue.dispatch(group: group) { 
	// count is 1 
	group.enter() 
	// count is 2 
	someAsyncMethod { 
		defer { group.leave() }

		// 여기서 작업을 수행하십시오.
		// 완료되면 카운트가 1로 돌아갑니다.
	}
}
  • 위의 코드 샘플과 비슷한 간단한 경우 enter / leave 쌍을 직접 호출하면됩니다.
  • 디스패치 그룹과 함께 someAsyncMethod를 자주 사용하려는 경우 필요한 호출을 잊지 않도록 메소드를 랩핑해야합니다.
func myAsyncAdd( 
	lhs: Int, 
	rhs: Int, 
	completion: @escaping (Int) -> Void
) { 
	// Lots of cool code here 
	completion(lhs + rhs) 
}

func myAsyncAddForGroups(
	group: DispatchGroup, 
	lhs: Int, 
	rhs: Int, 
	completion: @escaping (Int) -> Void
) { 
	group.enter()

	myAsyncAdd(first: first, second: second) { result in 
		defer { group.leave() } 
		completion(result) 
	}
}
  • 래퍼 메서드는 그룹에 대해 계산할 매개 변수를 취한 다음 나머지 인수는 래핑하는 메서드의 인수와 정확히 동일해야합니다.
    • 그룹의 출입 방법이 올바르게 처리되도록 100 % 확신하는 것 외에 비동기 방법을 포장하는 데 특별한 것은 없습니다.
  • 랩퍼 메소드를 작성하면 테스트를 수행 할 수 있습니다.
    • 모든 활용에서 적절한 발신 및 발신 통화 쌍을 확인하기 위해 단일 위치로 단순화되었습니다.

Downloading Images

  • 귀하의 작업은 제공된 이름 배열에서 각 이미지를 비동기 방식으로 다운로드하는 것입니다.
  • 완료되면 하나 이상의 이미지를 표시하고 놀이터를 종료해야합니다.
for id in ids { 
	guard let url = URL(string: "\(base)\(id)-jpeg.jpg") else { continue }

	group.enter()

	let task = URLSession.shared.dataTask(with: url) { data, _, error in
		defer { group.leave() }
		
		if error == nil, 
			let data = data, 
			let image = UIImage(data: data) { 
				images.append(image)
		} 
	}

	task.resume()
}
  • enter와 leave 쌍을 올바르게 처리하기 때문에 더 이상 그룹이 들어올 때까지 회전하고 동기적으로 기다릴 필요가 없습니다.
  • 모든 이미지 다운로드가 완료되면 notify(queue :) 콜백 메소드를 사용하십시오.
  • for 루프 외부에 이 코드를 추가하십시오.
group.notify(queue: queue) { images[0]
	PlaygroundPage.current.finishExecution()
}
  • 지금 playground를 실행하고 사이드 바를 보면 각 작업이 시작되고 이미지가 다운로드되고 결국 첫 번째 이미지로 알림이 트리거되는 것을 볼 수 있습니다.

Semaphores (신호장치)

  • 공유 리소스에 액세스 할 수 있는 스레드 수를 실제로 제어해야 하는 경우가 있습니다.
  • 단일 스레드에 대한 액세스를 제한하는 읽기 / 쓰기 패턴을 이미 보았지만 전체 스레드 수를 계속 제어하면서 한 번에 더 많은 리소스를 사용할 수있는 경우가 있습니다.

 

  • 예를 들어 네트워크에서 데이터를 다운로드하는 경우 한 번에 다운로드 횟수를 제한 할 수 있습니다.

 

  • 디스패치 대기열을 사용하여 작업을 처리하고 디스패치 그룹을 사용하여 모든 다운로드가 완료된 시기를 알 수 있습니다.
  • 그러나 수신하는 데이터가 상당히 크고 처리하기에 무겁기 때문에 한 번에 네 번의 다운로드 만 허용하려고 합니다.

 

  • DispatchSemaphore를 사용하면 해당 사용 사례를 정확하게 처리 할 수 ​​있습니다.
  • 원하는 자원을 사용하기 전에, 단순히 동기 함수 인 wait 메소드를 호출하면 자원이 사용 가능할 때까지 스레드가 실행을 일시 정지합니다.
  • 아직 소유권을 주장한 것이 없으면 즉시 액세스 할 수 있습니다.
  • 다른 쪽이 가지고 있다면, 자신이 끝났다는 신호를 보낼 때까지 기다립니다.

 

  • 세마포어를 작성할 때 자원에 대한 동시 액세스 수를 지정하십시오.
  • 네 번의 네트워크 다운로드를 한 번에 활성화하려면 4를 전달하십시오.
  • 독점 액세스를 위해 리소스를 잠그려면 1 만 지정하면 됩니다.

 

  • 디스패치 큐를 지정하는 행 다음에 네 개의 동시 액세스를 허용하는 세마포어를 작성하십시오.
  • 10 번의 네트워크 다운로드 수행을 시뮬레이션하고 그룹을 사용하여 큐에 디스패치하는 루프를 작성하십시오.
  • 세마포어를 만든 직후 루프를 구현하십시오.
let semaphore = DispatchSemaphore(value: 4)

for i in 1...10 { 
	queue.async(group: group) {
		semaphore.wait() 
		defer { semaphore.signal() }

		print("Downloading image \(i)")

		// Simulate a network wait 
		Thread.sleep(forTimeInterval: 3)

		print("Downloaded image \(i)")
	}
}
  • 디스패치 그룹에서 leave()를 호출해야 했던 것처럼 리소스 사용이 완료되면 signal()를 보내야합니다.
  • 리소스를 사용하지 않고 떠날 수있는 방법이 없으므로 defer 블록을 사용하는 것이 가장 좋습니다.

 

  • playground를 실행하면 즉시 4 번의 다운로드가 발생하고 3 초 후에 또 다른 4 번이 발생한다는 것을 즉시 확인할 수 있습니다.
  • 마지막으로 3 초 후에 마지막 2번이 완료됩니다.

Semaphore added to Downloading Images

  • 제어하려는 리소스는 네트워크이므로 URL을 for 루프에서 만들 수 있지만 그룹에 들어가기 전에 사용 가능한 세마포어를 기다려야하므로 group.enter() 전에 세마포어 호출 만 추가하면됩니다.
  • 세마포어가 자원에 대한 액세스를 제어하고 디스패치 그룹은 완료를 추적하는 방법이므로 두 요소를 모두 사용해야 합니다.
  • 세마포어의 릴리스를 처리하도록 defer 명령문을 수정하십시오.
...
	group.enter()
	semaphore.wait()

	let task = URLSession.shared.dataTask(with: url) { data, _, error in
		defer { 
			group.leave()
			semaphore.signal()
		}
		
		...
  • 줄의 순서는 실제로 중요하지 않습니다.
  • 나는 단지 세마포어가 작업을 시작하고 끝내는 외부 요소가 되도록 하고 싶습니다.

 

  • 세마포어 자체를 어떤 유형의 자원으로 생각하십시오.
  • 세 개의 망치와 네 개의 톱을 사용할 수있는 경우 두 개의 세마포어를 만들어 이를 나타냅니다.
let hammer = DispatchSemaphore(value: 3) 
let saw = DispatchSemaphore(value: 4)

댓글