[iOS] GCD (5) - Groups & Semaphores

전성훈·2023년 10월 31일
0

iOS/Concurrency

목록 보기
5/11
post-thumbnail

주제: 비동기 작업의 그룹화


DispatchGroup

  • 비동기로 작동하는 작업들이 모두 동시에 실행되지는 않지만 모두 완료되었을 때 행동해야 할 떄가 있다. 이러한 경우에는 Dispatch Groups을 활용한다.
  • DispatchGroup 클래스는 작업 그룹의 완료를 확인하는 경우로 사용할 수 있다.
  • DispatchGroup 을 초기화한 후에는 dispatch queue의 async 메서드에 그룹을 인자로 제공하여 해당 그룹의 작업을 확인할 수 있다.
  • DispatchGroup의 대표적인 예시로는
    1. 여러 개의 비동기 작업을 모니터링하여 모든 작업이 완료되면 다음 작업을 수행하도록 하는 행동 즉, 다수의 네트워크 요청을 동시에 보내고, 모든 요청이 완료되면 UI를 업데이트 하거나 다른 작업을 수행하는 등의 상황에서 유용하다. (이미지를 동시에 다 다운받고 확인할 때, 런치스크린과 첫 화면 예시)
    2. 다수의 큰 작업을 작은 조각으로 분할하여 각각을 병렬로 처리하고, 전체 작업이 완료되었는지를 추적하여 알리는 행동 즉, 대규모 데이터 처리 또는 이미지/동영상 처리 등의 작업
let group = DispatchGroup()
someQueue.async(group: group) { ... }
someQueue.async(group: group) { ... }
someOtherQueue.async(group: group) { ... }

group.notify(queue: DispatchQueue.main) { [weak self] in 
	self?.textLabel = text = "All jobs have completed"
}
  • notify 메서드는 dispatchQueue를 매개변수로 사용한다. 모든 작업이 완료되면 제공된 클로저가 지정된 디스패치 큐에서 실행된다.

Synchronous waiting

  • 어떤 이유로 그룹의 완료 알림에 비동기적으로 응답 할 수 없는 경우, 대신 dispatch Group에서 wait 메서드를 사용할 수 있다. 이는 모든 작업이 완료 될 때까지 현재 대기열을 차단하는 동기적 방법이다.
  • 작업이 완료 될 때까지, 얼마나 오래 기다릴지 기다리는 시간을 지정하는 optional 파리미터가 필요하다.
  • 지정하지 않으면 무제한으로 대기한다.
  • 메인 큐에서는 wait 메서드를 호출하면 안 된다.
let group = DispatchGroup() 
let queue = DispatchQueue.global(qos: .userInitiated)

queue.async(group: group) { 
	print("Start job 1")
	Thread.sleep(until: Date().addingTimeInterval(10))
	print("End job 2")
}

queue.async(group: group) { 
	print("Start job 2")
	Thread.sleep(until: Date().addingTimeInterval(2))
	print("End job 2")
}

if group.wait(timeout: .now() + 5) == .timedOut { 
	print("I got tired of waiting")
} else { 
	print("All the jobs have completed")
}
  • 위 코드는 비동기로 2개의 작업이 거의 동시에 시작되고 2초뒤에 2번째 작업이 종료, 5초 뒤에 wait 메서드로 인해 "I got tired of waiting"이 출력된다. 즉, 1번 작업은 10초가 걸리기 때문에 wait 메서드로 5초 기다리다가 5초가 지난 후 에도 작업이 완료되지 않았기 때문에 타임아웃 메시지가 출력된다.
  • 만약 wait 메서드가 중간에 있다면 어떨까?
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)

queue.async(group: group) {
    print("Start job 1")
    Thread.sleep(until: Date().addingTimeInterval(10))
    print("End job 1")
}

group.wait()

queue.async(group: group) {
    print("Start job 2")
    Thread.sleep(until: Date().addingTimeInterval(2))
    print("End job 2")
}
  • 이렇게 되면 1번째 작업이 끝날때까지 group은 무한정 대기한다.
  • 그 후 1번 작업이 끝나자마자 2번 작업을 작동시킨다.
  • 하지만 이와 같이 동기적인 대기 방법을 사용하는 것은 추후 문제점을 나타낼 가능성이 있으므로 코드를 최적화하기 위해 비동기적인 방법을 사용하는 것이 좋다.

Wrapping asynchronous methods

  • DispatchQueue는 기본적으로 DispatchGroup과 함께 작동할 수 있도록 내부에서 처리되며, 작업이 완료되었다는 것을 시스템에 신호로 보내는 역할을 한다. 여기서 완료되었다는 것은 코드 블록이 실행을 완료한 것을 의미한다.
  • 하지만, 클로저 내에서 비동기 메서드를 호출하면, 내부 비동기 메서드가 완료되기 전에 클로저가 완료된다.
  • 그렇기 때문에 내부 호출이 완료될 때까지 클로저가 완료되지 않았음을 알려줘야 한다. 이러한 경우에는 DispatchGroup에서 제공하는 enter와 leave 메서드를 호출할 수 있다.
  • 이 메서드들은 실행 중인 작업 수를 간단하게 세는 것과 같다. enter를 호출할 때마다, 실행 중인 작업 수가 1씩 증가하고, leave를 호출할 때 마다, 실행 중인 작업 수가 1씩 감소한다.
queue.dispatch(group: group) { 
	// count is 1
	group.enter()
	// count is 2
	someAsyncMethod { 
		defer { group.leave() }
		
		// Perfrom your work here.
		// count goes back to 1 once complete
	}
}

Downloading images

  • 네트워크 다운로드는 항상 비동기 작업으로 수행해야 한다.
let group = DispatchGroup()

// unsplash.com 무료 이미지 사이트
let base = "https://images.unsplash.com/photo-"

let imageNames = [
    "1579962413362-65c6d6ba55de?ixlib=rb-1.2.1&auto=format&fit=crop&w=934&q=80","1580394693981-254c3aeded6a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3326&q=80", "1579202673506-ca3ce28943ef?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=80", "1535745049887-3cd1c8aef237?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=80", "1568389494699-9076492b22e7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=937&q=80",  "1566624790190-511a09f6ddbd?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=80"
]

let userQueue = DispatchQueue.global(qos: .userInitiated)


var downloadedImages: [UIImage] = []

// for 문으로 다 도는 시간이 엄청 빠름
// enter로 모든 항목 +되고, URLSession clousre 내부 안에서 활동은 시간이 좀 걸림
// 그 항목들이 하나식 끝날 때마다 group이 마이너스 되다가
// group이 다 끝나면 notify 발동
for name in imageNames {
    guard let url = URL(string: "\(base)\(name)") else { continue }
    
    group.enter()
    
    // URL세션 자체가 비동기적 작업의 처리라는 것을 인식할 필요
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        // defer로 클로저의 마지막에 사용하도록 등록할 수있음
        defer { group.leave() }
        if error == nil, let data = data, let image = UIImage(data: data) {
            downloadedImages.append(image)
        }
    }
    task.resume()
}


group.notify(queue: userQueue) {
    downloadedImages
    print("=====모든 다운로드 완료=====")
    PlaygroundPage.current.finishExecution()
}
  • 위 작업은 image를 전부다 다 다운 받으면 아래 활동을 시작한다는 의미.
  • 여기서 스크롤할때마다 계속해서 데이터를 다운 받는 경우가 발생하는데, 이는 Image데이터를 cache 메모리에 저장해서 불필요한 다운로드 작업을 안 하도록 하는 방법이 필요하다.

Semaphores

  • 공유 리소스에 액세스할 수 있는 스레드 수를 제어해야 하는 경우가 있다. read/write 패턴을 사용하여 단일 스레드의 액세스를 제한하는 방법을 살펴보왔지만, 전체 스레드 수를 제어하면서 작업하는 방법 또한 필요하다.
  • DispatchSemaphore의 wait 메서드는 동기 함수이며, 스레드가 리소스를 사용할 수 있을 때까지 실행하고 다 사용했다면 일시 중지하고 기다리게 한다.
  • 어떤 스레드도 리소스를 소유하지 않은 경우 즉시 액세스할 수 있다.
  • 이미 모든 리소스를 소유하고 있다면 리소스를 소유하고 있는 스레드가 끝났다는 신호를 보내기 전까지 대기한다.
  • Semaphore를 만들 때 동시 액세스를 허용할 수 있는 수를 지정할 수 있다.
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
//: # 디스패치세마포어(DispatchSemaphore)
//: ### 수기신호: 공유리소스에 접근가능한 작업 수를 제한해야하는 경우

let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)

// unsplash.com 무료 이미지 사이트
let base = "https://images.unsplash.com/photo-"

let imageNames = [
    "1579962413362-65c6d6ba55de?ixlib=rb-1.2.1&auto=format&fit=crop&w=934&q=80","1580394693981-254c3aeded6a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3326&q=80", "1579202673506-ca3ce28943ef?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=80", "1535745049887-3cd1c8aef237?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=80", "1568389494699-9076492b22e7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=937&q=80",  "1566624790190-511a09f6ddbd?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=80"
]

let userQueue = DispatchQueue.global(qos: .userInitiated)

let downloadSemaphore = DispatchSemaphore(value: 4)

var downloadedImages: [UIImage] = []

for name in imageNames {
    guard let url = URL(string: "\(base)\(name)") else { continue }
    
    group.enter()
    downloadSemaphore.wait()
    
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        defer {
            downloadSemaphore.signal()
            group.leave()
        }
        
        if error == nil, let data = data, let image = UIImage(data: data) {
            downloadedImages.append(image)
        }
    }
    task.resume()
}



group.notify(queue: userQueue) {
    print("=====모든 다운로드 완료=====")
    downloadedImages
    PlaygroundPage.current.finishExecution()
}
  • 위의 작업은 한번에 4번씩 image data를 다운로드 받는다.

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글