UIKit

iOS | GCD에 관하여 - 2

ziziDev 2024. 7. 16. 00:08
반응형

Dispatch Group

 

 

 

 

//DispatchGroup 객체를 생성
let dispatchGroup = DispatchGroup()

//그룹에 비동기 작업을 추가할 때 dispatchGroup.enter()를 호출
dispatchGroup.enter()

//작업 완료 알림 (leave)
dispatchGroup.leave()

//그룹 내 모든 작업 완료 시점에 후속 작업 실행 (notify)
dispatchGroup.notify(queue: .main) {
    print("모든 작업 완료")
}

 

예를 들어 카카오톡 사진첩의 여러 장의 사진과 동영상을 동시에 공유하게된다면

DIspatchGroup을 사용할 수 있지 않을까 생각했습니다

 

장점

개별 작업 완료도를 추적할 수 있는 점 

단점

그룹카운털르 수동으로 증가하거나 감소해야하므로 코드가 장황해지고 

중첩된 완성 클로저로 인해 코드 가독성이 떨어지고 콜백함수로 인해서 수동적으로 오류 전달과정이 필요합니다

 

struct Media {
    let url: String
    var type: MediaType
    var data: Data?
}

enum MediaType {
    case photo
    case video
}


import UIKit

class ViewController: UIViewController {
    
    let dispatchGroup = DispatchGroup()
    var mediaList: [Media] = [
        Media(url: "https://via.placeholder.com/150", type: .photo),
        Media(url: "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4", type: .video),
        Media(url: "https://via.placeholder.com/200", type: .photo)
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        downloadMedia()
    }
    
    func downloadMedia() {
        for (index, media) in mediaList.enumerated() {
        //enter, leave를 사용해서 비동기 작업의 시작과 완료를 표시
            dispatchGroup.enter()
            download(from: media.url) { [weak self] data in
                self?.mediaList[index].data = data
                self?.dispatchGroup.leave()
            }
        }
        //모든 미디어가 다운로드하면 저장/공유 작업을 수행하게됨
        dispatchGroup.notify(queue: .main) { [weak self] in
            self?.saveOrShareMedia()
        }
    }
    
    //url 데이터를 다운로드하고 완료시 콜백함수 호출
    func download(from url: String, completion: @escaping (Data?) -> Void) {
        guard let url = URL(string: url) else {
            completion(nil)
            return
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            completion(data)
        }.resume()
    }
    
    //저장/공유
    func saveOrShareMedia() {
        for media in mediaList {
            if let data = media.data {
                switch media.type {
                case .photo:
                    savePhoto(data: data)
                case .video:
                    saveVideo(data: data)
                }
            }
        }
        print("모든 미디어 파일 저장 또는 공유 완료")
    }
    
    func savePhoto(data: Data) {
        if let image = UIImage(data: data) {
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            print("사진 저장 완료")
        }
    }
    
    func saveVideo(data: Data) {
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
        do {
            try data.write(to: tempURL)
            UISaveVideoAtPathToSavedPhotosAlbum(tempURL.path, nil, nil, nil)
            print("동영상 저장 완료")
        } catch {
            print("동영상 저장 실패: \(error)")
        }
    }
}

 

 

 

만약 여기 내부에서도 동기 비동기로 나뉘게 된다면??....

종료시점을 알기 위해서 카운팅하는 코드를 통해서 알 수 있습니다 

let count = dispatchGroup.debugDescription.components(separatedBy: ",").filter({$0.contains("count")}).first?.components(separatedBy: CharacterSet.decimalDigits.inverted).compactMap{Int($0)}.first

DispatchWorkItem

 

클로저로 보내왔던 작업이 캡슐화된 class라고 생각하시면 됩니다

 

let exam = DispatchWorkItem(qos: .utility) {
  print("Task 1")
  print("Task 2")
}
DispatchQueue.global().async(execute: exam)

exam.perform() //sync 동작

exam.cancel() //작업취소 - 작업이 실행전에는 제거 / 실행중 - 멈추지는 않고 inCallceld true /

 

 


동시성문제

 

Race Condition

경쟁 상태는 두 개 이상의 스레드가 공유 자원에 동시 접근할 때 발생하게됩니다

사실상 읽기는 상관이 없는데 쓰기 읽기 중구난방으로 동시에 접근한다면 머리아파지죠?..

 

그래서 동시성에서는 actor를 사용해서 경쟁상태를 방지할 수 있습니다

actor는 인스턴스의 상태를 보호하고 동시접근을 관리하기 때문입니다

import UIKit

actor Counter {
    var value = 0

    func increment() {
        value += 1
    }
}

class RaceConditionViewController: UIViewController {
    var label = UILabel()
    let counter = Counter()

    override func viewDidLoad() {
        super.viewDidLoad()
        label.frame = CGRect(x: 50, y: 50, width: 200, height: 50)
        self.view.addSubview(label)

        Task {
            await self.updateCounter()
        }

        Task {
            await self.updateCounter()
        }
    }

    func updateCounter() async {
        for _ in 0..<1000 {
            await counter.increment()
            DispatchQueue.main.async {
                self.label.text = "Counter: \(self.counter.value)"
            }
        }
    }
}

Deadlock(교착상태)

 

배우기전 세마포어에 대해서 알면 좋을것 같습니다

세마포어를 사용하면 교착 상태와 같은 상태가 발생할 수 있고 멀티스레딩 환경에서는 설계가 좋지 않으면

교착 상태가 발생할 수 있습니다

하지만 스레드 간의 리소스를 조절하고 동기화하는데 유용합니다

 

 

 

동시 작업의 개수제한을 위해서

iOS에서는 DispatchSemaphore라는 객체를 제공하고

다수의 스레드가 특정 이벤트를 완료 상태를 동기화 하는데 유용합니다

 

//공유 작업 접근 가능한 작업 수 10개 제한
let semaphore = DispatchSemaphore(value: 10)

//특정 이벤트 완료 상태 동기화
//콜백함수같은느낌
//한 동작이 끝나고 무언가 실행할 때

let exam = DispatchSemaphore(value: 0)

DispatchQueue.global(qos: .background).async {
	print("1")
    print("2")
    print("3")
    
    semaphore.signal()
}

semaphore.wait()

 

Arvind Seth

누가 먼저 리소스를 해제할 지 기다리는 상태에대해서 교착상태라고 합니다

더보기
  1. 상호 배제 Mutual exclusion
  2. 최소한 한 자원은 반드시 비공유 모드여야 함. 한번에 한 스레드만 자원 사용 가능. 다른 스레드가 자원을 요청하면, 그 스레드는 자원이 방출될 때까지 대기
  3. 점유하며 대기 Hold-and-wait
  4. 스레드는 최소한 한 자원을 점유한 상태에서 다른 스레드가 붙잡고 있는 추가적인 자원을 위해 대기해야 함
  5. 비선점 No preemption
  6. 자원이 강제로 방출될 수 없고, 점유하고 있는 스레드가 태스크를 종료한 후 자발적으로 방출해야 함
  7. 순환 대기 Circular wait
  8. 대기 중인 스레드 집합 { T0, …, Tn }에서 T0이 T1이 붙잡고 있는 자원을 기다리고, Tn - 1은 Tn이 붙잡고 있는 자원을 기다리고, Tn은 T0이 붙잡고 있는 자원을 기다림

교착상태가 해결될 때까지 한 프로세스에 자원을 몰아주는데 여기서 세 가지 사항들을 고려해 볼 수 있다 여기서 어떤 프로세스를 죽일것이고 어떤 프로세스의 자원을 선점하여 다른 프로세스에 할당을 해줄것이고 이 부분에서는 비용측면에서 많이 보기 때문에 매번 걸리는 프로세스만 희생자로 걸리는 경우에 기아현상이 발생 할 수 있을 수 있기에 비용측면에서만 생각하지 않고 다양한 측면을 통해서 고려해야한다

 

교착 상태 탐지 : 공습경보 공습경보.. 교착 상태 일어났다! 교착 상태로부터 회복 : 걱정마세요 그에 따라 적절한 사후 조치를 취하겠습니다 ⇒ 교착상태가 탐지되면 시스템은 몇몇 프로세스들을 종료하거나 몇몇 프로세스들은 자원을 선점함으로써 회복

Priority Inversion(우선순위 역전)

우선순위 역전은 낮은 우선순위의 스레드가 높은 우선순위의 스레드보다 먼저 자원을 점유하게 되는 현상

동시성에서 일의 우선순위를 명시적으로 설정할 수 있습니다

 

예시로 높은 순위의 일이 낮은 우선순위 일을 점유하고 있는 자원을 기다리는 경우에 역전 현상이 발생합니다

해결방법은 우선순위 상속 메커니즘을 구현하거나 우선순위를 설정을 하여서 관리한는 방법입니다

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 네트워크 요청을 백그라운드 큐에서 수행합니다.
        DispatchQueue.global().async {
            let url = URL(string: "https://api.example.com/data")!
            let data = try? Data(contentsOf: url)
            let json = try? JSONDecoder().decode([String: String].self, from: data!)
            let message = json?["message"] ?? "데이터를 가져올 수 없습니다."

            // UI 업데이트를 메인 스레드에서 수행합니다.
            DispatchQueue.main.async {
                self.label.text = message
            }
        }
    }
}

 

네트워크 요청이 느릴경우에 UI Update 처리하기 위해 대기하게 되는데

다른 중요한 UI작업(사용자 입력처리)를 지연할 수 있기 때문에

너무 느려지는 경우엔 앱이 응답하지 않거나 충돌날 수 있습니다

그래서 네트워크 시간 제한 설정이나 UI 업데이트 우선순위를 높여서 방지할 수 있고

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // 백그라운드 작업을 수행합니다.
        DispatchQueue.global().async {
            let image = loadImage(name: "image.png")
            let processedImage = applyFilter(image: image)

            // UI 업데이트를 메인 스레드에서 수행합니다.
            DispatchQueue.main.async {
                self.imageView.image = processedImage
            }
        }
    }

    func loadImage(name: String) -> UIImage? {
        // ... 이미지 로드 코드
    }

    func applyFilter(image: UIImage) -> UIImage {
        // ... 이미지 필터 적용 코드
    }
}

 

백그라운드 큐에서 UI업데이트 작업 수행하지 않아야 발생가능성이 적어집니다

 

더보기

 

  • 네트워크 요청 시간 제한 설정: DispatchQueue.global().async(timeout: deadline, execute: ) 함수를 사용하여 네트워크 요청 시간 제한을 설정
  • UI 업데이트 우선순위 높이기: DispatchQueue.main.async(priority: .high, execute: ) 함수를 사용하여 UI 업데이트 작업의 우선순위를 높일 수 있음

 

 

 

Deadlock with Dispatch Semaphore

In this article we will understand about deadlock and how deadlock can happen using semaphore in iOS, but it does not mean that we should…

medium.com

 

반응형