본문 바로가기

Swift/GCD

WWDC 2016 - Concurrent Programming With GCD in Swift 3

Concurrency allows multiple parts of your application to run at the same time.

Concurrency(동시성)는 우리의 앱을 동시적으로 여러 부분을 같은 시간에 실행되도록 돕는다.

 

시스템단에서는, 동시성을 구현하기 위해 thread를 생성한다. CPU 코어는 주어진 시간 동안 스레드를 생성할 수 있다. 그러나 동시성 구현에 대한 payoff, 혹은 페널티는 스레드를 안전하게 유지하기 힘들다는 것이다.

GCD

GCD는 애플의 동시성 라이브러리, 이는 모든 iOS 기기들의 multi-threaded한 코드를 생성하는데 도움을 준다.

 

Dispatch queue and Run Loops

Dispatch queue는 말 그대로 queue에 dispatch(제출)하는것. Dispatch 할 것은 당연히 작업 (closure이자 dispatchWorkItem) 일 것이다. 이 distpatch queue는 스레드와 서비스를 가져올 것이다. 그리고 스레드에서 작업을 마치면 해당 스레드를 자동으로 종료할 수 있다.

사용자가 직접 스레드를 생성할 수 있다. 그리고 스레드에서 run loop를 실행할 수 있다. 메인 스레드는 run loop와 main queue를 모두 가질 수 있다.

Dispatch queue는 작업을 제출하는 두가지 방법이 있다.

 

Asynchronous Excution

첫번쨰 방법은 asynchronous(비동기) 실행이다.

Dispatch queue에 여러 작업을 정렬하고, 스레드를 불러와서 작업을 처리한다. Dispatch queue는 queue에서 작업을 하나씩 처리한다.

 

그런 다음 queue의 작업이 완료되면, 시스템은 스레드를 해제한다.

 

Synchronous Excution

다시 위 예시와 같은 비동기 작업이 포함된 dipatch queue가 있다. 스레드에서 코드(작업)를 실행하고 이 작업이 마칠 때까지 큐가 기다리길 원한다. 해당 작업을 dispatch queue에 제출하면 해당 queue는 block(대기상태에 진입)되고, 실행을 요청한 항목이 완료될 때까지 대기한다.

 

해당 queue에 비동기 작업을 추가할 수 있고, 그러면 dispatch queue는 스레드를 가져와 해당 작업을 처리한다. async 작업이 실행하고 난 뒤, sync 작업이 실행되어야 할 때는 dispatch queue가 대기하고 있던 스레드에게 제어권을 넘기고 해당 작업을 실행 한 뒤, worker 스레드로 dispatch queue의 제어권을 가져온다.

 

나머지 작업들도 큐에서 처리하고, 그다음 스레드를 해제한다.

 

그렇다면 이를 통해 user interface를 차단하는 작업(리소스가 큰 변환작업)을 메인스레드에서 분리하려면, 해당 작업을 다른 queue에서 실행하면 된다. 그러면 변환 작업을 dispatch queue와 함께 지원할 수 있다. 이제 데이터의 변환작업을 하려면, 해당 변환할 데이터를 다른 큐에서 작성한 변환 코드블록으로 이동시키고, 변환한 뒤 다시 메인 스레드로 보내면 된다.

 

// Dispatch Queue 생성
let queue = DispatchQueue(label: "com.example.imagetransform")

// Dispatch Queue에 비동기 작업 제출 
queue.async {
    let smallImage = image.resize(to: rect)

		// Dispatch main queue 로 메인 스레드에서 작업 실행
    DispatchQueue.main.async {
      imageView.image = smallImage
		} 
}

 

Controlling Concurrency

애플리케이션에서 동시성을 제어해야 할 때가 있다. dispatch(GCD 라이브러리 전체를 일컫음)가 사용하는 스레드 풀은 기기의 모든 콜에서 사용하기 위해 동시성을 제한한다.

그러나 만약 애플리케이션의 다른 부분이나 시스템 호출에서 워커 스레드를 block하면, Dispatch는 코드를 계속 실행하기 위하여 block 된 워커 스레드에서 더 많은 워커 스레드를 생성해 동시성을 제공하려고 할 것이다.

따라서 코드를 실행하기 위해 사용할 dispatch queue의 갯수를 올바르게 선택해야 한다. 그렇지 않으면 하나의 스레드를 block, 다른 스레드 생성, 다시 block, 또다시 생성과 같은 패턴이 반복되며 이는 thread explosion을 발생시킨다.

(WWDC 2015 - Building Responsive and Efficient Apps with GCD - https://kswift.tistory.com/21 참조)

 

Structuring Your Application

메인 스레드에서 작업을 다른 큐로 가져와 동시에 작업을 처리하는 것이 효율적이라는 것을 확인했다. 이를 효율적으로 적용하기 위해서 어떻게 해야할까?

 

이전의 방법은 독립적인 data flow와 함께 애플리케이션의 영역을 구분하고 이를 하위 시스템으로 분리하여 dispatch queue 로 지원했다. 이는 각 하위 시스템마다 각각의 작업을 실행할 queue가 제공된다. (Chaining)

또 다른 방법은 작업을 그룹화 하고 해당 작업이 완료될 때까지 기다리는 것이다. 하나의 항목이 다른 여러 작업들을 생성한다면, 이 작업들이 완료될 때까지만 진행하려 한다면 이 또한 dispatch를 통해 구현할 수 있다. (Grouping)

 

Grouping Work Together

DispatchGroup은 작업을 추적할 수 있게 도와준다. 생성은 단순히 DispatchGroup 객체를 생성하면 된다.

그리고 dispatch 에 작업을 제출할 때, 옵셔널 파라미터로 그룹을 추가할 수 있다. 이것은 각 다른 큐에서 실행될 수 있지만, 같은 그룹으로 연관 지어진다. 그리고 그룹에 작업을 추가할 때마다, 그룹은 작업의 counter를 추가해 완료를 대기한다. 그리고 제출한 작업이 모두 완료되면, 그룹에서 notify를 통해 선택한 queue에 완료를 알릴 수 있다.

순차적으로 작업은 실행되고, 작업이 완료될 때마다 counter는 줄어든다. 그리고 마지막 작업이 완료되면 그룹은 notification block에서 notify를 처리한다.

 

Synchronizing Between Subsystems

마지막 방법은 동기 실행을 통해 하위 시스템간 상태를 직렬화하는 방법이다.

이는 상호 배제(동기화된) 프로퍼티를 사용할 수 있다. 이것은 작업을 queue에 동기적으로 제출했을 때, 각 작업을 동시에 실행하지 않도록 한다.

이는 간단한 thread safe 프로퍼티 접근을 구축할 수 있다. 이것은 동기화 큐를 호출하고 동기화 큐에서 값을 캡처하고 반환한다. 그러나 이것은 각 실행 시스템 간에 lock 그래프가 생기게 된다. 이는 교착 상태가 발생할 여지가 생긴다.

 

Using Quality of Service Classes

WWDC 2015 - Building Responsive and Efficient Apps with GCD 에서 QoS에 대해 설명했기 때문에 간단히 변경점만 설명

간단히 async 메서드에 QoS 파라미터를 추가해 준다.

만약, 더 높은 QoS의 작업을 뒤에 제출한 경우, 우선순위 역전을 해결할 수 있다. 간단히 dispatch queue에서 앞선 작업의 QoS를 올려 해당 작업이 더 빨리 실행 되록한다. 이것은 작업을 더 빨리 실행하도록 도와주는 것이 아니라, 제출한 작업과 함께 앞선 모든 작업을 우선적으로 실행시킨다는 의미이다.

또한 특정 QoS클래스의 dispach queue를 생성할 수도 있다. 예를 들어, 항상 백그라운드에서 실행해야 될 작업이라면, 해당 작업을 모두 실행하는 queue를 생성할 수 있다. 해당 queue에 작업을 제출하면, 그 작업은 해당 QoS를 가진다. 더 세분화된 수준에서 dispatch queue에 비동기로 호출하면, 호출하는 부분에서 실행 컨텍스트(execution context)를 캡처한다.

이것에 대한 더 많은 제어를 위해서 DispatchWorkItem을 생성하여 더 많은 제어권을 가진 work item을 생성할 수 있다.

 

예를 들어 assignCurrentContext flags 인 work item을 생성했다면, 이것은 dipatch queue에 제출되는 시점의 QoS가 아닌, 생성되는 시점을 중심의 QoS를 가져온다. 이것은 제출할 때 생성한 시점의 속성을 가지고 dipatch에 제출할 work item을 생성할 수 있다.

또한, DispatchWorkItem의 wait메서드를 사용해 dispatch에게 진행하기 전, 작업을 완료해야 함을 알릴 수 있다. wait메서드를 사용하면, 해당 작업이 속한 queue에 대해서 대기 중인 작업 이전의 작업들을 해당 queue의 QoS에 맞게 높여 실행한다. 이렇게 함으로써 대기 중인 작업의 우선순위에 맞춰 실행이 가능해진다.

이는 DispatchWorkItem이 자신이 어떤 dispatch queue에 제출된 것인지 알고 있기 때문에 가능한 것인데 Semaphore와 Groups를 사용해 대기하는 경우에는 이런 정보를 저장하지 않기 때문에 wait메서드를 호출하더라도 대기 중인 작업 이전의 작업들을 높여 실행시키지 않기 때문에, 대기 중인 작업이 이전 작업들보다 먼저 실행되는 것을 보장할 수 없다.

 

(DispatchWorkItem에 대한 더 자세한 정보들은 https://kswift.tistory.com/20 에 정리해 놨습니다)

Synchronization

Swift의 프로퍼티들은 non atomic 하기 때문에 만약, lazy프로퍼티를 같은 순간에 init 한다면 아마 두 번 불릴 것이다. 따라서 동기화가 필요하다.

(이부분은 https://kswift.tistory.com/19 에 Race Condition 에서 다뤘습니다)

동기화 시점을 잊어버린다면, 앱에서 크래시나 데이터가 혼선되는 일이 벌어질 것이다.

 

전통적으로 동기화를 위해 lock을 사용했다. Swift는 C구조체 기반의 락(lock)을 구현하여 사용할 수 있다.

그러나, Swift에서는 모든 것이 이동 가능하다고 가정하기 때문에, 뮤텍스(mutex)나 락(lock)과 같은 것은 작동하지 않을 수 있다. 따라서 Swift에서 이러한 종류의 전통적인 C구조체 기반의 Lock(pThread_mutex_t)을 사용하지 않는 것이 좋다.

 

💡 이동 가능하다의 개념 (값복사 vs 참조복사)
메모리에서 복사되어 전달되는 것이 아니라, 소유권이 전달되어 메모리에서 이동되는 것

  • 함수나 메서드 등에서 값이 반환될 때, 반환값은 복사 대신 이동될 수 있다
  • 값이 변수에서 다른 변수로 할당될 때도 이동될 때 이를 통해 메모리 사용량을 최소화하면서도 값을 안전하게 공유할 수 있다.

 

전통적인 lock을 사용하려면, 클래스 기반의 Foundation.Lock을 사용할 수 있다.

그러나, 위에서 보이는 것과 같은 Object-C의 기본 클래스를 도입해야 한다.

 

Use GCD for Synchronization

GCD를 통하여 동기화를 하면 전통적인 lock을 사용하는 것에 비해서 잘못 사용할 여지가 더 줄어든다. 코드는 범위가 지정된 방식으로 실행되기 때문에 실행이 완료되면 자동으로 lock이 해제된다. 따라서 잠금을 해제하는 것을 신경 쓰지 않아도 된다.

그리고 queue는 실제로 XCode의 런타임 디버깅이 더 잘 통합된다.

 

간단히 위와 같은 방법으로 동기화를 구현할 수 있다.

 

Precondition

이것은 주어진 큐에 불변식이 코드에 있다는 것을 나타낸다. 현재 주어진 큐에서 실행 중인지, 혹은 현재 큐가 아닌지를 나타내는 전제조건이다.

Object Lifecycle in a Concurrent World

동기화가 필요한 일부 객체를 처리하는 방법

  1. 객체를 생성하고, 속성을 설정한다. - Setup
  2. 객체를 활성화한다. 다른 서브시스템에서 이 객체를 사용할 수 있도록 만드는 것이다. 이제 이 객체를 더 많은 동시성 작업을 수행할 수 있는 성능적인 역할을 가진다. - Activated
  3. 객체를 무효화한다. 이는 모든 하위 시스템이 이 객체가 사라지고 있다는 것을 알게 하는 것이다. - Invalidated
  4. 객체가 소멸된다. - Deallocation

 

출처

https://developer.apple.com/videos/play/wwdc2016/720/?time=1070