본문 바로가기

iOS/Concurrency

[Swift] DispatchWorkItem, barrier와 singleton 개념 정리!! | No5. GCD

안녕하세요.

이번 포스트는 GCD의 개념 중 하나인 DispatchWorkItem, DispatchGroup, DispatchSemaphore에 대한 개념을 정리하려고 합니다.

 

GCD나 concurrency의 개념은 링크에서 개념 정리 했습니다!!

GCD 관련 포스트로 개념 정리했습니다.

 

Dispatch queue에 task를 보내는 방법으로 async, sync가 있었습니다. () -> Void 타입의 클로저의 형태를 async의 인자값에 넣음으로 해당 task가 queue에 추가되서 thread에서 실행 됬습니다. DispatchWorkItem도 이와 유사합니다. 추가적으로 많은 기능을 지원합니다. task에 관해 더 많은 설정을 하고 싶을 때 DispatchWorkItem을 사용합니다. enqueued된 task의 수행을 delay, canel등등..

 

1. DispatchWorkItem's concept

https://developer.apple.com/documentation/dispatch/dispatchworkitem

 

GCD의 task를 효율적으로 다루고 수행하기 위한 개념 DispatchWorkItem. class입니다. DispatchWorkItem을 사용해 캡슐화를 해서 dispatch queue나 dispatch group에 추가해서 캡슐화된 task를 수행하는 개념입니다. DispatchSource event, registration, cancel관련 처리를 할 수 있습니다.

 

https://developer.apple.com/documentation/dispatch/dispatchworkitem/2300102-init

 

디폴트로 dispatch queue's async, sync 함수를 사용할 때와 같이 사용하면 됩니다. block은 @escaping 입니다. dispatch queue나 dispatch group에 .async로 제출 시 실행 context를 capture합니다.

 

qos는 이전 포스트에서 다뤘습니다. block이 dispatch queue에 추가될 때 우선순위에 의해 먼저 수행하는 개념입니다.

 

DispatchWorkItemFlags는 block에 대한 qos, barrier, spawn등을 설정할 수 있습니다.

block에 task가 들어갑니다. async, sync함수와 마찬가지로 () -> Void 타입의 클로저를 입력하면 됩니다.

 

DispatchWorkItemFlags에선 barrier, assignCurrentContext가 잘 쓰이는 것 같습니다. 

assignCurrentContext를 사용할 경우 dispatch queue나 group에 DispatchWorkItem을 보낼 때 지정된 QoS가 아니라 DispatchWorkItem이 생성될 때 지정된 QoS가 capture되어 사용됩니다. 

 

 

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2

Barrier의 경우 GCD. concurrent 수행에서 일어날 수 있는 shared resource를 동시에 읽으면서 쓸 때 문제가 발생할 사용됩니다. + 싱글톤을 만들 경우에도 사용됩니다. barrier를 통해 concurrency 상황에서 특정한 task만 serial하게 실행시키도록 할 수 있습니다. barrier를 사용해서 한 개 또는 여러 개의 task에 대해 dispatch queue에서 sync한 수행을 하도록 사용할 수 있습니다. concurrency queue에서 사용해야 합니다. 여러 개의 task가 동시에 실행되는 dispatch's concurrent queue에서 DispatchWorkItem의 barrier task가 추가된다면 barrier 이전에 수행되던 task가 전부 실행 될 때까지 barrier task의 실행을 지연합니다. Barrier이전의 task가 전부 실행되면 비로소 단독으로 barrier task가 실행됩니다. Barrier task가 실행될 때에 한해서만 serial queue처럼 동작합니다. Barrier task가 끝나면 다시 concurrency하게 task들이 수행됩니다.

 

즉 concurrent 타입의 dispatch queue에서 synchronization 실행이 가능합니다. with DispatchWorkItem(flags: .barrier){ ... }

2. How to use DispatchWorkItem?

async()함수에 task 추가하는 방법

let queue = DispatchQueue(label: "async_func")
queue.async {
    print("task 추가")
}

 

DispatchWorkItem을 통해 dispatch queue에 task 추가 하는 방법

/// 인자값 옵션 세팅 할 경우 DispatchWorkItem(qos: .background, flags: .barrier) { ... }
let workItem = DispatchWorkItem { [weak self] in
    print(" task 로직 추가! ")
}
DispatchQueue.global().async(execute: workItem)
DispatchQueue.global().asyncAfter(deadline: .now(), execute: workItem)
/// task 취소
workItem.cancel()

async(execute:)

asyncAfter(deadline:execute:)

등의 함수로 task(DispatchWorkItem)를 추가할 수 있습니다. 이는 위의 async 코드와 같은 원리로 dispatch queue의 task로 enqueue됩니다.

 

cancel() 함수를 통해 task를 취소할 수 있습니다.

queue에서 아직 thread를 통해 실행되지 않은 경우 cancel을 사용하면 workItem이 제거됩니다. task가 진행중일 경우 취소가 안됩니다.

 

notify(queue:execute:)를 통해 다음 task 예약도 가능합니다.

 

3.  DispatchWorkItem and barrier!!

동시성, 멀티 쓰레딩에서 발생할 수 있는 문제 중 하나로 readers-writers problem이 있다고 했습니다. singleton 객체의 공유 자원을 사용할 때 반드시 해결해야 합니다. Fabrizio Brancati 저자가 쓴 singleton에서 공유 자원 문제가 발생했을 때 barrier를 통해 해결했는데 이를 풀이해보려고 합니다.(원래 잘 설명되어있습니다.. _ 거의 하단 page 입니다.)

 

Q> 싱글톤은 언제 사용되는가?

싱글톤의 개념은 여러 viewController에서 사용되는 인스턴스의 객체가 오직 1개 인 것을 의미합니다. 누구나 같은 인스턴스를 사용하기 위해 동시에 접근하려고 할 때 만들면 좋습니다. 하지만 단순히 클래스에 선언 -> static으로 인스턴스화 해서 만들고 동시에 접근해서 reader가 되는 것은 문제가 없습니다. 싱글톤 객체의 자원을 읽는 사람이 있는데 동시에 수정하려고 한다면 문제가 발생합니다.

 

Thread-safe의 개념이 지켜진다면 어디에서 호출되던 코드의 실행 결과는 동일합니다. thread-safe의 개념이 지켜지기 위해선 공유자원을 최대한 줄이거나, 세마포어등의 lock을 통해 사용중이 명확하게 식별되야 합니다.

싱글톤에서 thread-safe에 대해 주의해야 할 점은

1. 인스턴스를 초기화 할  때(여러곳에서 초기화 하게 된다면 기존에 있던 정보 날라가게 됩니다.)

2. 인스턴스에 대한 읽기 쓰기 문제가 발생한 경우

두 가지 입니다.

 

이전에 초기화 관련해서 고민을 했었습니다. Static에 대한 개념을 여기서도 활용할 수 있게 되어 다행이네요.Static 객체의 초기화는 단 한번됩니다. 프로그램 시작시 되는 것은 아니고 main thread가 해당 코드의 초기화 구문을 실행했을 때 단 한번 메모리에 할당되어 초기화가 됩니다. 그 이후에 초기화가 안됩니다. 이미 메모리에 할당됬기 때문입니다. 그래서 static은 thread-safe 입니다.

 

하지만 concurrency한 환경에선 NSLock을 사용하지 않는다면 문제가 발생합니다. 싱글톤의 공유 자원에 접근하려는 메서드 관련해서는 computed property를 통한 읽기 전용, barrier를 통한 concurrent -> synchronization으로 변경해서 작업을 수행해야 합니다.

 

final class PhotoManager {
    private init() {}
    static let shared = PhotoManager()
    private var _photos: [Photo] = []
}

위에서 언급한 바와 같이 static은 단 한번 초기화가 됩니다. PhotoManager()는 단 한번 실행이 됩 니다. 그리고 메모리에 할당되어 더이상 PhotoManager()를 호출하지 않습니다.

 

extension PhotoManager{

    var photo: [Photo] {
    	return _photo
    }
    
    func addPhoto(_ photo: Photo) {
    	/// 1.
        _photos.append(photo)
        DisptchQueue.main.saync { [weak self] in
            self?.postContentAddedNotification()
        }
    }
}

_photos는 변경 가능한 array입니다. Collection type인 array, dictionary는 삽입, 삭제가 가능한 var의 경우에 not thread safe타입이 됩니다. computed property를 사용하는 것은 읽기, 쓰기가 가능 했던 _photo변수를 오직 읽기 전용으로 만듭니다. var타입의 변수가 let 타입의 변수가 되는 것과 마찬가지입니다. getter만 있는 computed peroperty이기 때문에 read-only가 됩니다. 즉 concurrent task들은 읽기만 가능해서 thread safe입니다. (공유 자원이 변경되지 않기 때문입니다.)

 

하지만 PhotoManager 내 addPhoto함수에서는 주석1 아래 라인의 코드를 통해 수정이 가능합니다. computed property를 사용한 이유가 없습니다. 읽는 동시에 값이 변경된다면 이상한 결과를 초래할 수 있습니다. concurrency한 task가 동시에 수정을 할 수 있기 때문입니다.

 

그래서 위에서 언급한 바와 같이 NSLock을 통해서 문제를 해결하거나, 세마포어를 만든다. 또는 dispatch barrier를 이용하는 방법이 있습니다. 수정할 때 아무도 리소스를 사용하지 않을 때 까지 기다렸다가 수정하는 방법(using barrier). 읽을 때 한 사람씩 읽는 방법,,

 

final class PhotoManager{
...
	private let photoQueue = DispatchQueue(
    	    	                label:"com.raywenderlich.GooglyPuff",
                                attributes: .concurrent)
}

이 dispatchqueue를 통해 photo 관련 기능을 수행하는 싱글 톤을 사용할 때의 task들을 관리합니다. ( 물론 이 싱글톤을 사용하는 task가 몰리면 reader-writer문제가 발생합니다. 이를 barrier를 통해 해결합니다.

 

func addPhoto(_ photo: Photo) {
	/// 1.
	let item = DispatchWorkItem(flags: .barrier) { [weak self] in
    	guard let self = self else { return }
        
        /// 2.
        self._photo.append(photo)
        
        DispatchQueue.main.async { [weak self] in
            self?.postContentAddedNotification()
        }
    }
    
    /// 3.
    photoQueue.async(execute: item)
}

1. DispatchWorkItem 생성. 

2. DispatchWorkItemFlags 타입이 .barrier이기 때문에 이 task가 실행될 때는 _photo 배열을 읽는 사람x. reader-writer 문제 x

3. photoQueue에 추가.

 

var photos: [Photo] {
	var photosCopy: [Photo] = []
    
   ///1.
    photoQueue.sync {
        ///2.
    	photosCopy = self._photos
    }
    return photosCopy
}

1. 읽기 작업을 수행한다면 photoQueue에 추가합니다( 나 읽고 있어요~)

2. read-only로 inner property _photos 값을 반환합니다.

 

음.. 운영체제를 다시 복습해야겠다는 생각이 드네요.

 

참고 자료:

https://www.kodeco.com/28540615-grand-central-dispatch-tutorial-for-swift-5-part-1-2

https://medium.com/geekculture/concurrency-in-ios-gcd-c3a69acd7f31

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

https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/

https://stackoverflow.com/questions/58038406/how-can-we-make-static-variables-thread-safe-in-swift