본문 바로가기
iOS/System

Concurrency Problems - Concurrency by Tutorials

by 탄이. 2020. 6. 14.

Chapter 5: Concurrency Problems

Concurrency by Tutorials
In this part of the book, you're going to learn about the basics of Concurrency. You're going to learn what it is, what kind of problems it solves, and why would you even use it? Further, you will learn the basic pieces of which Concurrency comprises in Cocoa development: Grand Central Dispatch and Operations.
https://store.raywenderlich.com/products/concurrency-by-tutorials
  • 앱에서 동시성을 구현할 때 주의하지 않으면 발생할 수있는 잘 알려진 세 가지 문제가 있습니다.
    1. Race conditions
    1. Deadlock
    1. Priority inversion

Race conditions (경쟁조건)

  • 앱 자체를 포함하여 동일한 프로세스를 공유하는 스레드는 동일한 주소 공간을 공유합니다.
  • 이것은 각 스레드가 동일한 공유 리소스를 읽고 쓰려고 한다는 것을 의미합니다.
  • 조심하지 않으면 여러 스레드가 동시에 같은 변수에 쓰려고 하는 경쟁 조건에 처할 수 있습니다.

 

  • 두 개의 스레드가 실행 중이고 둘 다 객체의 개수 변수를 업데이트 하려는 예를 고려하십시오.
  • 읽기와 쓰기는 컴퓨터가 단일 작업으로 실행할 수 없는 별도의 작업입니다.
  • 컴퓨터는 클럭의 각 틱이 단일 작업을 실행할 수 있는 클럭 사이클에서 작동합니다.

 

  • 스레드 1과 스레드 2는 모두 카운트를 업데이트하려고 할 때 다음과 같이 멋진 코드를 작성하십시오.
count += 1
  • 무해한 것 같아요? 그 진술을 구성 요소 부분으로 나누고 손맛을 조금 더하면 다음과 같은 결과가 나옵니다.
    1. 가변 카운트 값을 메모리에 로드합니다.
    1. 메모리에서 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 (스레드 장벽)

  • 때때로, 당신의 공유 자원은 단순한 변수 수정보다 그것의 gettersetter에 더 복잡한 논리를 요구합니다.
  • 온라인에서 이와 관련된 질문을 자주 볼 수 있으며, 잠금 및 세마포어와 관련된 해결책이 함께 제공되는 경우가 많습니다.
  • 잠금은 제대로 구현하기 매우 어렵습니다.
  • 대신 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
  • 최종 결과는 항상 같습니다. 우선 순위가 높은 작업은 우선 순위 반전으로 인해 중간 및 낮은 우선 순위 작업 후에 항상 실행됩니다.

댓글