본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #41 동기화에는 락보다는 디스패치 큐를 사용하라

by 돼지왕 왕돼지 2017. 10. 6.
반응형

 [Effective Objective-C] #41 동기화에는 락보다는 디스패치 큐를 사용하라


출처 : Effective Objective-C

@synchronized, barrier, block 복사, DeadLock, dispatch_async, dispatch_barrier_async, dispatch_barrier_sync, dispatch_block_t, dispatch_get_global_queue, dispatch_queue_create, DISPATCH_QUEUE_PRIORITY_DEFALUT, dispatch_queue_t, dispatch_sync, GCD, getter, LOCK, lock gcd, nslock, nsrecursivelock, Queue, serial queue, serial synchronization queue, Setter, Unlock, [Effective Objective-C] #41 동기화에는 락보다는 디스패치 큐를 사용하라, __block, 데드락, 동기 디스패치, 동기화, 디스패치 큐, 락, 배리어, 배타적 실행, 베리어, 병렬 큐, 병렬큐 베리어 블록, 순차적 동기화 큐, 순차적 큐, 오래 걸린다, 유지보수, 재귀 락, 저수준, 최적화


-

다수 스레드에서 동시에 접근하기 때문에 문제를 겪는 코드를 오브젝티브-C에서 가끔 발견할 수 있다.

이 문제를 해결하기 위해서는 앱이 락을 사용해 동기화해야 한다.

GCD 이전에는 동기화를 위한 두 가지 방법이 있었는데 첫 번째 방법은 built-in 동기화 블록이다.

- (void) synchronizedMethod{

     @synchronized(self){

          // 코드

     }

}


이 구조는 주어진 객체를 기반으로 락을 자동으로 생성하고 블록에 포함된 코드가 완료될 때까지 락을 잡고 기다린다.

락은 코드 블록의 끝에서 풀린다.



-

또 다른 방법은 NSLock 객체를 직접 사용하는 것이다.

_lock = [[NSLock alloc] init];


-(void) synchronizedMethod{

     [_lock lock];

     // 코드

     [_lock unlock];

}


NSRecursiveLock 을 사용하면 재귀적인 락도 가능하다.

이는 한 스레드가 데드락으로 빠지지 않고 동일한 락을 여러 번 가져갈 수 있게 한다.



-

위의 두 방법 모두 괜찮지만 다 문제점이 있다.

동기화 블록은 극한환경에서 발생하는 데드락 때문에 고역이고,

이를 구현하기 위해 효율적이지 않은 코드를 작성하느라 힘들 것이다.

락을 직접 사용하는 것은 데드락이 발생하면 골칫거리가 될 것이다.


대안은 GCD 를 사용하는 것이다.

락을 훨씬 간단하고 효율적인 방법으로 쓸 수 있다.



-

동기화 블록 또는 락 객체를 사용하는 것의 간단하고 효과적인 대안은 순차적 동기화 큐(serial synchronization queue)를 사용하는 것이다.

동일한 큐에서 읽기, 쓰기를 가져오면(즉 동기화할 대상에 대한 읽기, 쓰기 오퍼레이션 권한) 동기화가 보장된다.

다음과 같이 할 수 있다.

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);


-(NSString*)someString{

     __block NSString *localSomeString;

     dispatch_sync(_syncQueue, ^{

          localSomeString = _someString;

     });

     return localSomeString;

}


-(void)setSomeString:(NSString*)someString{

     dispatch_sync(_syncQueue, ^{

          _someString = someString;

     });

}


세터와 게터 둘 다 실행되는 GCD 큐가 순차적 큐(serial queue)이다.

변수를 설정하기 위해 블록을 사용하는 게터는 __block 문법 외에는 꽤 깔끔하다.

모든 잠금(locking)은 GCD 가 다룬다.

이는 매우 저수준으로 구현되었고 많은 최적화가 이루어져 있다.

그렇기 때문에 여러분은동기화가 어떻게 이루어지는지 신경 쓸 필요 없이 접근자 코드를 작성하는 데만 집중하면 된다.



-

세터는 동기화가 필요하지 않다.

인스턴스 변수를 설정하는 블록은 세터 메서드로 어떠한 것도 반환하지 않는다.


즉..

-(void)setSomeString:(NSString*)someString{

     dispatch_async(_syncQueue, ^{

          _someString = someString;

     });

}


동기화 디스패치를 비동기 디스패치로 바꾸는 이 간단한 변경으로 호출자 관점에서는 세터가 훨씬 빨라진 것처럼 느껴지는 장점이 생긴다.

그러나 읽기와 쓰기는 여전히 각각에 대해 순차적으로 이루어진다.

유일한 단점은 이를 시험해봤다면 알겠지만 비동기 디스패치는 실행될 블록을 복사해야 하기 때문에 동기 디스패치에 비해 오래 걸린다는 것이다.

블록을 실행할 때 걸리는 시간과 비교하여 복사하는 시간이 더 걸린다면 동기화 디스패치보다 더 느릴 것이다.

그래서 우리 예제에서는 비동기 디스패치가 더 느릴 것이다.

그러나 디스패치된 블록이 오래 걸리는 일을 수행한다면

여전히 비동기 디스패치는 매력적인 대안일 것이다.



-

베리어(barrier)라는 간단한 GCD 기능을 이용해 병렬 Queue 를 활용할 수도 있다.

큐 베리어 블록 함수는 다음과 같다.

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);


배리어는 큐의 모든 다른 블록과는 배타적으로 실행된다.

그것들은 오직 병렬 큐에서만 관련이 있다.

모든 순차 큐의 블록은 항상 서로가 배타적으로 실행되기 때문이다.

큐가 처리되고 다음 블록이 배리어 블록이면 큐는 모든 현재블록이 끝나기를 기다린다.

그런 다음 배리어 블록을 실행한다.

배리어 블록의 실행이 끝나면 큐 처리는 이전처럼 이어진다.



-

세터가 배리어 블록을 사용하더라도 프로퍼티를 읽는 것은 여전히 병렬도 실행된다.

그러나 쓰기는 배타적으로 실행할 것이다.

_syncqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFALUT, 0);


-(NSString*)someString{

     __block NSString *localSomeString;

     dispatch_sync(_syncQueue, ^{

          localSomeString = _someString;

     });

     return localSomeString;

}


- (void)setSomeString:(NSString*)someString{

     dispatch_barrier_async(_syncQeue, ^{

          _someString = someString;

     });

}


순차 큐를 사용하는 것보다 빨라짐을 알 수 있을 것이다.

또 세터에서 동기화 베리어도 사용할 수 있다.

이는 방금 설명한 것 같은 이유로 훨씬 효율적이다.

신중히 각 방법의 성능을 테스트하고 자신에게 가장 적합한 것을 고르라.




기억할 점


-

동기화 문법을 제공하기 위해 디스패치 큐를 사용할 수 있다.

그리고 그냥 간단하게 @synchronized 블록이나 NSLock 객체를 이용해 동기화를 제공할 수도 있다.



-

동기화와 비동기화 디스패치를 함께 사용하는 것은 일반적인 락으로 하는 동기화와 동일한 기능을 제공할 수 있지만,

스레드에서 비동기 디스패치로 호출하면 잠금 없이 호출된다.



-

병렬 큐와 베리어 블록을 이용하면 동기화를 좀 더 효율적으로 할 수 있다.




반응형

댓글0