[iOS] GCD #4

RudinP·2024년 7월 24일
0

Study

목록 보기
257/258

경합 상황에서 동기화 방법으로 문제 해결하기


import UIKit

class ViewController: UIViewController {
    
    var value = 0
    
    let firstQueue = DispatchQueue(label: "First", attributes: .concurrent)
    let secondQueue =  DispatchQueue(label: "Second", attributes: .concurrent)
    
    let group = DispatchGroup()
    
    @IBOutlet weak var valueLabel: UILabel!
    
    @IBAction func start(_ sender: Any) {
        value = 0
        
        for _ in 1 ... 1000{
            firstQueue.async(group: group, execute: {
                self.value += 1
            })
            
            secondQueue.async(group: group, execute: {
                self.value += 1
            })
        }
        
        group.notify(queue: .main){
            self.valueLabel.text = "\(self.value)"
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

이러한 상황에서 우리는 2000이 출력될 것이라고 생각한다.
하지만 동기화가 되지 않았기 때문에 결과는 기대와는 다르게 나온다.

두 개의 스레드에서 동시에 동일한 값에 접근하면 결과는 알 수 없어진다.

Race Condition과 Critical Section

위와 같은 문제를 Race Condition(경합 상황)이라고 한다.
그리고 Race Condition이 발생하는 범위를 Critical Section이라고 한다.

for _ in 1 ... 1000{
	firstQueue.async(group: group, execute: {
		self.value += 1 //Critical Section
	})
...

항상 똑같은 결과를 얻고 싶다면 속성을 동기화해야 한다.

동기화란?

  • Critical Section에 동시에 접근하지 못하게 하고 일정한 순서대로 접근하게 하여 Race Condition이 발생하지 않도록 하는 행위

Serial Queue로 동기화

  • sync를 사용하여 직렬화한다.
let firstQueue = DispatchQueue(label: "First") //concurrent 사용해도 상관없음
let secondQueue =  DispatchQueue(label: "Second")
    
let syncQueue = DispatchQueue(label: "Sync")
@IBAction func start(_ sender: Any) {
        value = 0
        
        for _ in 1 ... 1000{
            firstQueue.async(group: group, execute: {
                self.syncQueue.sync { //sync로 직렬화
                    self.value += 1
                }
            })
            
            secondQueue.async(group: group, execute: {
                self.syncQueue.sync {
                    self.value += 1
                }
            })
        }
        
        group.notify(queue: .main){
            self.valueLabel.text = "\(self.value)"
        }
    }

Mutex

  • Mutual Exclusion의 약자
  • Critical Section에 접근할 수 있는 스레드를 하나로 제한하는 방식
  • 애플에서는 NSLock 클래스와 DispatchSemaphore로 구현 가능하다.

  • NSLock에서 unlock 메소드를 사용할 수 있는것은 해당 리소스를 lock한 스레드뿐이며, 다른 스레드가 unlock 메소드를 사용 시 crash가 발생하니 주의하자.
  • 여기서 atomically라는 뜻은 1. 실행이 시작되면 중간에 중단할 수 없으며 중간에 다른 연산이 끼어들 수 없음, 2. 실행 결과가 다른 결과에 독립적임 을 의미한다.

  • lock: 이미 다른 스레드가 lock중이라면 unlock될때까지 대기한다. 따라서 이 메소드를 메인스레드에서 호출할 때는 조심해야 한다.
  • unlock: 이전에 잠갔던 부분을 해제한다.

  • lock과 비슷한 역할을 하지만, 스레드를 블로킹하지 않는다는 특징이 있다.
  • lock이 가능하다면 true, 아니면 false를 리턴한다.
  • 따라서 메인스레드에서 사용할 때는 lock보다는 try를 사용해야 한다.
  • 대신 lock이 될때까지 기다리지 않기 때문에 주기적으로 try를 시도하도록 구현해야 한다.

동기화 구현

  • critical section 전에 lock을 하고 처리 후 unlock
	 let lock = NSLock()
    @IBAction func mutex(_ sender: Any) {
        value = 0
        
        for _ in 1 ... 1000{
            firstQueue.async(group: group, execute: {
                self.lock.lock()
                self.value += 1
                self.lock.unlock()//lock한 스레드와 같은 스레드에서 unlcok 필수
                
            })
            
            secondQueue.async(group: group, execute: {
                self.lock.lock()
                self.value += 1
                self.lock.unlock()
            })
        }
        
        group.notify(queue: .main){
            self.valueLabel.text = "\(self.value)"
        }
    }

Semaphore

  • count 기반의 동기화 기법
  • 공유 리소스에서 접근할 수 있는 스레드의 수를 사용자가 제어 가능
  • 소유권의 개념이 없어 mutex와 달리 signal과 wait을 같은 스레드에서 호출할 필요는 없다. 다만, 쌍으로 호출해야 함에는 변함없음을 유의하자.

  • init에 전달하는 정수는 공유 리소스에 접근할 수 있는 스레드의 수를 전달
  • 이 값을 보통 count Value라고 부른다.
  • 스레드가 접근 시 count value가 1씩 감소하고 접근을 끝내면 다시 count value가 1씩 증가하는 원리이다.

  • wait 메소드는 count 가 0보다 크다면 count value를 감소시키고 바로 리턴하고, 그렇지 않으면 0보다 큰 값이 될때까지 스레드를 블로킹하며 대기한다.
  • signal 메소드는 count value를 증가시키므로 리소스의 사용이 끝났을 때 호출한다.

동기화 구현

  • critical section 전에 wait, 처리 후 signal
	let sem = DispatchSemaphore(value: 1)
    @IBAction func semaphore(_ sender: Any) {
        value = 0
        
        for _ in 1 ... 1000{
            firstQueue.async(group: group, execute: {
                self.sem.wait()
                self.value += 1
                self.sem.signal()
                
            })
            
            secondQueue.async(group: group, execute: {
                self.sem.wait()
                self.value += 1
                self.sem.signal()
            })
        }
        
        group.notify(queue: .main){
            self.valueLabel.text = "\(self.value)"
        }

    }
profile
iOS 개발자가 되기 위한 스터디룸...

0개의 댓글