Chapter 5: Concurrency Problems
- 앱에서 동시성을 구현할 때 주의하지 않으면 발생할 수있는 잘 알려진 세 가지 문제가 있습니다.
- Race conditions
- Deadlock
- Priority inversion
Race conditions (경쟁조건)
- 앱 자체를 포함하여 동일한 프로세스를 공유하는 스레드는 동일한 주소 공간을 공유합니다.
- 이것은 각 스레드가 동일한 공유 리소스를 읽고 쓰려고 한다는 것을 의미합니다.
- 조심하지 않으면 여러 스레드가 동시에 같은 변수에 쓰려고 하는 경쟁 조건에 처할 수 있습니다.
- 두 개의 스레드가 실행 중이고 둘 다 객체의 개수 변수를 업데이트 하려는 예를 고려하십시오.
- 읽기와 쓰기는 컴퓨터가 단일 작업으로 실행할 수 없는 별도의 작업입니다.
- 컴퓨터는 클럭의 각 틱이 단일 작업을 실행할 수 있는 클럭 사이클에서 작동합니다.
- 스레드 1과 스레드 2는 모두 카운트를 업데이트하려고 할 때 다음과 같이 멋진 코드를 작성하십시오.
count += 1
- 무해한 것 같아요? 그 진술을 구성 요소 부분으로 나누고 손맛을 조금 더하면 다음과 같은 결과가 나옵니다.
- 가변 카운트 값을 메모리에 로드합니다.
- 메모리에서 1 씩 카운트 값을 증가시킵니다.
- 새로 업데이트 된 카운트를 디스크에 다시 씁니다.
- 위의 그래픽은 다음과 같습니다.
- 스레드 1은 스레드 2 전에 클럭 사이클을 시작하고 카운트에서 값 1을 읽습니다.
- 두 번째 클럭 사이클에서 스레드 1은 메모리 내 값을 2로 업데이트하고 스레드 2는 카운트에서 값 1을 읽습니다.
- 세 번째 클록 사이클에서 스레드 1은 이제 계수 2에 값 2를 다시 씁니다.
- 그러나 스레드 2는 이제 메모리 내 값을 1에서 2로 업데이트하고 있습니다.
- 네 번째 클록 사이클에서 스레드 2는 이제 카운트 2에 값 2를 기록합니다.
- 두 개의 별도 스레드가 값을 업데이트했기 때문에 값 3을 볼 것으로 예상한 것과 다릅니다.
- 일반적으로 발생하는 것을 알고 있는 한 직렬 대기열을 사용하여 경쟁 조건을 해결할 수 있습니다.
- 프로그램에 동시에 액세스 해야 하는 변수가 있는 경우 다음과 같이 private 큐로 읽기 및 쓰기를 랩핑 할 수 있습니다.
private let threadSafeCountQueue = DispatchQueue(label: "...") private var _count = 0 public var count: Int { get { return threadSafeCountQueue.sync { _count } } set { threadSafeCountQueue.sync { _count = newValue } } }
- 별도의 언급이 없기 때문에 threadSafeCountQueue는 직렬 대기열입니다.
- 즉, 한 번에 하나의 작업 만 시작할 수 있습니다. 따라서 변수에 대한 액세스를 제어하고 한 번에 하나의 스레드 만 변수에 액세스 할 수 있습니다.
- 위와 같이 간단한 읽기 / 쓰기를 수행하는 경우 이것이 최선의 해결책입니다.
Thread barrier (스레드 장벽)
- 때때로, 당신의 공유 자원은 단순한 변수 수정보다 그것의
getter
와setter
에 더 복잡한 논리를 요구합니다.
- 온라인에서 이와 관련된 질문을 자주 볼 수 있으며, 잠금 및 세마포어와 관련된 해결책이 함께 제공되는 경우가 많습니다.
- 잠금은 제대로 구현하기 매우 어렵습니다.
- 대신 GCD에 있는 애플의 디스패치 장벽 솔루션을 사용할 수 있습니다.
private let threadSafeCountQueue = DispatchQueue(label: "...", attributes: .concurrent) private var _count = 0 public var count: Int { get { return threadSafeCountQueue.sync { return _count } } set { threadSafeCountQueue.async(flags: .barrier) { [unowned self] in self._count = newValue } } }
- 동시 대기열을 원하고 쓰기를 장벽으로 구현해야 한다고 지정하는 방법에 주목하십시오.
- 이전의 모든 읽기가 완료 될 때까지 장벽 작업이 수행되지 않습니다.
- 장벽에 도달하면 큐는 그것이 직렬인 것처럼 가장하고 오직 장벽 작업만이 완료될 때까지 실행될 수 있습니다.
- 완료되면 차단 작업 후 제출 된 모든 작업을 다시 동시에 실행할 수 있습니다.
Deadlock (교착상태)
- 서로가 상대방이 자원을 내놓기를 바라면서 무기한 연기 상황에 빠지는 것교착 상태는 세마포어 또는 기타 명시적 잠금 메커니즘과 같은 것을 사용하지 않는 한 Swift 프로그래밍에서 거의 발생하지 않습니다.
- 실수로 현재 디스패치 큐에 대해 동기화를 호출하는 것이 가장 일반적인 경우입니다.
- 세마포어를 사용하여 여러 리소스에 대한 액세스를 제어하는 경우 동일한 순서로 리소스를 요청해야 합니다.
- 스레드 1이 망치와 톱을 요구하는 반면 스레드 2가 톱과 망치를 요구하면 교착 상태가 발생할 수 있습니다.
- 스레드 1은 동시에 요청하고 망치를 받고 스레드 2는 톱을 요청하고 받습니다.
- 그런 다음 스레드 1은 망치를 풀지 않고 톱을 요구하지만 스레드 2는 리소스를 소유하므로 스레드 1은 기다려야 합니다.
- 스레드 2는 톱을 요청하지만 스레드 1은 여전히 리소스를 소유하므로 스레드 2는 톱이 사용 가능할 때까지 기다려야 합니다.
- 두 스레드는 요청된 자원이 풀려날 때까지 진행할 수 없기 때문에 이제 교착 상태에 빠져 있습니다.
Priority inversion (우선순위 반전)
- 기술적으로 말하자면, 우선 순위 반전은 서비스 품질(QoS)이 낮은 큐에 서비스 품질이 높은 큐 또는 QoS보다 높은 시스템 우선 순위가 제공 될 때 발생합니다.
- 대기열에 작업을 제출하는 작업을 가지고 놀았다면, 당신은 아마도 qos 매개 변수를 취하는 비동기화 생성자를 알림받았을 것이다.
- 3 장 "큐 및 스레드"에서 큐의 QoS는 제출 된 작업에 따라 변경 될 수 있다고 언급했습니다.
- 일반적으로 작업을 대기열에 제출하면 대기열 자체의 우선 순위를 갖습니다.
- 그러나 필요한 경우 특정 작업의 우선 순위가 정상보다 높거나 낮도록 지정할 수 있습니다.
.userInitiated
큐와.utility
큐를 사용하고,.userInteractive
서비스 품질 (.userInitiated
보다 우선 순위가 높은)을 사용하여 후자의 큐에 여러 태스크를 제출하면 운영 체제에 의해 후자의 큐에 우선 순위가 높은 상황이 발생할 수 있습니다.
- 후자의 큐는 운영 체제에 의해 더 높은 우선 순위가 지정됩니다.
- 갑자기 대기열에 있는 모든 작업, 그 중 대부분은 실제로
.utility
품질의 서비스인데, 결국.userInitiated
큐의 작업보다 먼저 실행될 것입니다.
- 이를 피하기는 간단합니다. 보다 높은 서비스 품질이 필요한 경우 다른 대기열을 사용하십시오!
- 우선 순위 반전이 발생하는 보다 일반적인 상황은 서비스 품질이 높은 큐가 서비스 품질이 낮은 리소스를 공유하는 경우입니다.
- 하위 대기열이 객체를 잠그면 상위 대기열을 기다려야합니다.
- 잠금이 해제 될 때까지 우선 순위가 낮은 작업이 실행되는 동안 우선 순위가 높은 큐는 아무 것도 수행하지 않습니다.
- 이 코드에는 QoS 값이 다른 세 개의 스레드와 세마포가 표시됩니다.
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 {
// 다른 모든 작업이 대기열에 들어가도록 2 초 정도 기다리십시오.
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)
}
- playground를 실행하면 실행할 때마다 다른 순서가 표시됩니다.
Running medium task 7
Running medium task 6
Running medium task 1
Running medium task 4
Running medium task 2
Running medium task 8
Running medium task 5
Running medium task 3
Running medium task 9
Running medium task 10
Running long, lowest priority task
High priority task is now running
- 최종 결과는 항상 같습니다. 우선 순위가 높은 작업은 우선 순위 반전으로 인해 중간 및 낮은 우선 순위 작업 후에 항상 실행됩니다.
댓글