[iOS] GCD (6) - Concurrency Problems

전성훈·2023년 10월 31일
0

iOS/Concurrency

목록 보기
6/11
post-thumbnail

주제: 비동기 코드 활용시 발생 할 수 있는 문제점들


주의해야 할 문제

Race conditions (경쟁 상황)

  • 같은 프로세스를 공유하는 스레드는 같은 주소 공간을 공유한다. 이것은 각 스레드가 동일한 공유 리소스를 읽고 쓰려고 시도한다는 것을 의미한다.
  • 이는 잘 못하면 여러 스레드가 동시에 동일한 변수에 접근하는 경합 조건에 빠질 수 있다.
  • 예를 들면, 두 개의 스레드가 실행 중이면서 두 스레드 모두 객체의 count 변수를 업데이트하려고 한다고 가정해볼 때 경쟁 상황을 잘 나타낼 수 있다.
    • 데이터 읽기와 쓰기는 컴퓨터가 단일 작업으로 실행할 수 없기 때문에 서로 다른 작업으로 처리된다.
    • 컴퓨터는 클럭 사이클에서 작동하며, 각 클럭 탭은 단일 작업을 실행할 수 있다.
    1. 클럭: 첫번째 스레드에서 값 불러오기
    2. 클럭: 첫번째 스레드에서 값 더하기(2), 두번째 스레드에서 값 불러오기(1)
    3. 클럭: 첫번째 스레드 값 저장하기(2), 두번째 스레드에서 값 더하기(2)
    4. 클럭: 두번째 스레드에서 값 저장하기(2)
    • 즉, 이처럼 더하기 작업을 2번 실행했지만, 각 상황이 겹쳐서 더하기 작업이 이상해진것을 확인할 수 있다.
var count = 0
for _ in 0...30000 { 
	DispatchQueue.global().async { 
		count += 1
	}
}
// 원래 값인 30001 아닌 엉뚱한 값이 출력됨
  • Race Conditions을 해결하는 방법 중 하나는 두 스레드가 동시에 같은 변수에 접근하게 되는 것을 방지하기 위해 시리얼 큐를 사용하는 것이다.
var count = 0 
for _ in 0...30000 { 
	DispatchQueue.global().sync { 
		count += 1
	}
}
// 정상적인 값을 보장받는다. 
  • 해결방법
    • 시리얼 큐 + Sync (엄격한 Thread-safe)
    • 디스패치 베리어 작업 (조금 더 효율적인 방법)
    • (추가적인, Semaphore 이용 동시실행자원도 제한 가능)

Deadlock (교착 상태)

  • 교착상태란, 여러 개의 프로세스나 스레드가 서로 상대방의 작업이 끝날 떄까지 기다리고 있어 무한정 기다리게 되는 상황을 말한다. 이렇게 되면 모든 작업이 멈추고, 시스템 자원이 낭비되며 작업이 중단된다.
  • Swift에서 교착상태는 주로 Semaphore나 다른 명시적인 locking 메커니즘을 사용할 때 발생할 수 있다.
  • 예를 들어, 두 개의 스래드가 서로 다른 두 개의 자원에 접근하려고 할 때, 스레드 1이 자원 A를 점유하고, 스레드 2가 자원 B를 점유한 후 스레드 1이 자원 B를 요청하고 스레드 2가 자원 A를 요청하는 경우 교착상태가 발생할 수 있다.
let semaphoreA = DispatchSemaphore(value: 1)
let semaphoreB = DispatchSemaphore(value: 1)

DispatchQueue.global().async {
    semaphoreA.wait()
    print("Thread 1: Acquired Semaphore A")
    sleep(1)
    semaphoreB.wait()
    print("Thread 1: Acquired Semaphore B")
    semaphoreB.signal()
    semaphoreA.signal()
}

DispatchQueue.global().async {
    semaphoreB.wait()
    print("Thread 2: Acquired Semaphore B")
    sleep(1)
    semaphoreA.wait()
    print("Thread 2: Acquired Semaphore A")
    semaphoreA.signal()
    semaphoreB.signal()
}
  • 해결방법

    (단순한 처리 방법) 시리얼큐로 해결 가능
    세마포어 등 제한된 리소스 순서 사용 / 객체 등 설계시 주의

Priority inversion (우선 순위의 뒤바뀜)

  • 우선순위 뒤바뀜은 높은 QoS를 갖는 큐보다 우선순위가 낮은 큐에 더 높은 시스템 우선순위가 부여되는 경우에 발생한다.
  • 즉, 높은 우선순위의 작업이 낮은 우선순위의 작업에 의해 대기하게 되는 현상을 의미한다. 이는 우선순위를 이용한 스케즐링의 목적에 위배되며, 시스템 성능에 영향을 미칠 수 있다.
  • 예를 들어, 공유 자원에 대한 접근을 동기화하는 동안 높은 우선순위의 작업이 낮은 우선순위의 작업을 기다리게 되면 우선 순위의 뒤바뀜 문제가 발생할 수 있다.
  • 시리얼큐에서 높은 우선순위 작업이 낮은 우선순위의 뒤에 보내지는 경우
  • 낮은 우선순위의 작업이 높은 우선순위가 필요한 자원을 잠그고 있는 경우(lock, 세마포어)
  • 높은 우선순위 작업이 낮은 우선순위 작업에 의존하는 경우(operation)
let high = DispatchQueue.global(qos: .userInteractive)
let medium = DispatchQueue.global(qos: .userInitiated)
let low = DispatchQueue.global(qos: .background)

let semaphore = DispatchSemaphore(value: 1)

high.async {
    // Wait 2 seconds just to be sure all the other tasks have enqueued
    Thread.sleep(forTimeInterval: 2)
    semaphore.wait()
    defer { semaphore.signal() }

    print("High priority task is now running")
}

for i in 1 ... 10 {
    medium.async {
        let waitTime = Double(exactly: arc4random_uniform(7))!
        print("Running medium task \(i)")
        Thread.sleep(forTimeInterval: waitTime)
    }
}

low.async {
    semaphore.wait()
    defer { semaphore.signal() }

    print("Running long, lowest priority task")
    Thread.sleep(forTimeInterval: 5)
}

Running medium task 4
Running medium task 5
Running medium task 2
Running medium task 7
Running medium task 1
Running medium task 9
Running medium task 3
Running medium task 10
Running medium task 6
Running medium task 8
Running long, lowest priority task
High priority task is now running
  • 해결방법

    GCD가 알아서 우선 순위 조정
    (안전하게) 공유된 자원 접근시 - 동일한 QoS 사용

Thread-Safety

  • 여러 스레드가 동시에 공유 자원(메모리, 파일)에 접근하거나 수정할 때, 프로그램이 올바르게 동작하는 것을 의미한다.
  • 동시적 처리를 하면서(여러 스레드를 사용하면서도) 문제없이 스레드를 안전하게 사용한다는 의미이다.
  • 데이터에 여러 스레드를 사용하여 접근하여도, 한번에 한개의 스레드만 접근가능하도록 처리하여 경쟁상황의 문제없이 사용이 가능하다.

출처(참고문헌)

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

감사합니다.

0개의 댓글