직접 Dispatch Queue 만들기

10059 단어
이 글은 @ 저는 써니라고 합니다. 왜 추천한 Mike Ash의 블로그에서 왔습니다. 주로 디스패치 queue의 기본 기능을 어떻게 직접 실현하는지 설명합니다.번역 과정에서 일부 부분을 약간 조정하다.처음 번역, 교정을 환영합니다!텍스트 주소: [https://www.mikeash.com/pyblog/friday-qa-2015-09-04-lets-build-dispatch_queue.html] 코드 주소: [https://github.com/mikeash/MADispatchQueue]
GCD는 애플이 최근 몇 년 동안 개발한 훌륭한 API 중 하나입니다. "Lets Bulid"시리즈의 최신 호에서 디스패치queue의 가장 기초적인 특성을 재실현하고 이 주제는 Rob Rix가 추천합니다.
디스패치 queue는 전역 스레드 탱크가 지원하는 작업 대기열을 요약합니다.전형적인 것은 작업이 백엔드 라인에서 다른 단계로 실행되는 대기열에 제출되는 것이다.모든 스레드는 하나의 단일한 백엔드 스레드 탱크를 공유하는데, 이것은 시스템을 더욱 효율적으로 할 수 있다.
이것은 내가 재현하고자 하는 이 API의 본질이다.GCD에서 제공하는 고급 기능을 간단하게 무시하기 위해서입니다.예를 들어 전역 탱크에서 스레드 수를 늘리고 줄이는 대량의 작업을 완성하고 시스템이 CPU에 대한 이용을 한다.만약 한 무더기의 작업이 CPU를 차지하고 다른 작업을 제출한다면 GCD는 그를 위해 다른 작업 라인을 만드는 것을 피할 것이다. 왜냐하면 CPU 사용률이 100%에 이르렀기 때문에 다른 라인의 작업은 저효과가 될 것이다.나는 이 점을 건너뛰고 라인의 수량에 대해 하드코딩을 사용할 것이다.나도 포지셔닝 대기열과 폐쇄 병발 대기열 같은 다른 특성을 생략할 것이다.
디스패치 queues의 본질인 직렬과 병발을 주목하는 것이 우리의 목표입니다. 그들은 동기화되거나 비동기적으로 임무를 발송할 수 있고 공유된 전역 스레드 탱크가 지원합니다.
인터페이스: GCD는 C 언어의 API입니다.GCD 대상은 이미 최근 OS 버전에서 OC 대상으로 전환됐지만 API는 순수한 C(애플 블록 추가 확장)를 유지하고 있다.이것은 아주 좋은 저층 API이다. GCD는 매우 뚜렷한 인터페이스를 제공했지만 나의 목표를 달성하기 위해 나는 차라리 OC로 재현하고 싶다.OC 클래스는 MADispatchQueue입니다. 그는 4개의 호출 방법만 있습니다. 공유 전역 대기열을 가져오는 방법입니다.GCD에는 여러 개의 서로 다른 우선순위의 전역 대기열이 있지만, 우리는 간단하기 위해 하나만 있습니다.병렬 또는 직렬 대기열을 만들 수 있도록 초기화하는 방법비동기식 dispatch에서 동기화된 dispatch 호출 방법에 대한 설명을 호출합니다.
@interface MADispatchQueue : NSObject 

+ (MADispatchQueue *)globalQueue; 
- (id)initSerial: (BOOL)serial;
 - (void)dispatchAsync: (dispatch_block_t)block;
 - (void)dispatchSync: (dispatch_block_t)block;
 @end
 

그 다음에 그들이 묘사한 일을 끝내라.
스레드 탱크 인터페이스 스레드 탱크에는 간단한 인터페이스 지원 대기열이 있습니다.그는 실제 운행 중인 이미 제출한 임무의 힘든 일을 할 것이다.대열은 정확한 시기에 그들의 대열에 임무를 제출할 수 있다.스레드 탱크에는 하나의 단일한 작업이 있습니다. 작업을 제출해서 실행합니다.따라서 인터페이스에는 단 하나의 방법이 있습니다.
@interface MAThreadPool : NSObject 
- (void)addBlock: (dispatch_block_t)block; 
@end

이것이 핵심이기 때문에, 우리가 먼저 그를 실현합시다.
스레드 탱크의 실현은 먼저 실례 변수를 본다.스레드 탱크는 내부와 외부를 포함하여 여러 스레드에 접근할 수 있으며 스레드 안전이 필요하다.GCD는 자신의 방법에서 벗어나 가능한 한 빠른 원자화 조작을 사용하고 나의 재건에서 나는 구식 자물쇠를 사용한다.나는 이 자물쇠가 신호를 기다리고 보낼 수 있어야 한다. 상호 배척만 하는 것이 아니라. 그래서 나는 일반적인 NSLock이 아닌 NSCondition을 사용했다.만약 당신이 그에 대해 익숙하지 않다면, NSCondition은 기본적으로 하나의 자물쇠와 하나의 단일 조건 변수의 봉인이라고 이해할 수 있다.NSCondition *_lock;
스레드 풀에 추가된 스레드 수, 실제 작업하는 스레드 수, 최대 스레드 수 등을 알아야 합니다.
NSUInteger _threadCount; 
NSUInteger _activeThreadCount;
NSUInteger _threadCountLimit;

결국 한 줄의 Block이 실행된다.NSMutable Array를 사용하여 마지막에 새 Block을 추가하고 처음부터 제거함으로써 대기열을 시뮬레이션합니다.
NSMutableArray *_blocks;

초기화 작업은 매우 간단하다.자물쇠를 초기화하고 Block 그룹을 초기화합니다. 임의의 수량으로 라인의 수량 제한을 설정합니다. 128:
- (id)init {
        if((self = [super init])) {
            _lock = [[NSCondition alloc] init];
            _blocks = [[NSMutableArray alloc] init];
            _threadCountLimit = 128;
        }
        return self;
    }

Blocks 그룹이 비어 있을 때까지 간단한 무한 순환에서 작업하는 루틴이 실행됩니다.가져올 수 있는 Block이 있으면, 이 Block은 그룹에서 열을 내고 실행됩니다.우리가 이렇게 할 때, 활동의 라인 수를 증가시키면, 끝날 때 수량을 줄여야 한다.
- (void)workerThreadLoop: (id)ignore {

첫 번째는 자물쇠를 얻는 것이다. 순환이 시작되기 전에 기억해야 한다.이유는 순환이 끝날 때 알게 될 것이다.
[_lock lock];

무한 순환:
while(1){

대기열이 비어 있으면 잠금이 대기 상태입니다.
while([_blocks count] == 0) {
 [_lock wait]; 
}

이것은if문장이 아니라 순환을 통해 완성해야 한다는 것을 기억해라.이유는 다음과 같습니다. [https://en.wikipedia.org/wiki/Spurious_wakeup] 간단하게 말하면wait 이 상태는signaled가 없어도return이 가능하기 때문에 이러한 행위를 수정하기 위해waitreturn이 있을 때 조건을 재검토해야 한다.일단 Block을 획득할 수 있다면, 계집아이의 열을 내보내기:
dispatch_block_t block = [_blocks firstObject];
[_blocks removeObjectAtIndex: 0];

활성 스레드 수를 늘려 활성 스레드가 활성 상태임을 나타냅니다.
_activeThreadCount++;

지금은 Block을 실행할 때가 되었지만, 우리는 먼저 자물쇠를 풀어야 한다. 그렇지 않으면 우리는 동시에 여러 가지 재미있는 자물쇠를 만들 수 없다.
[_lock unlock];

자물쇠를 안전하게 놓은 후 Block을 실행합니다:
block();

Block이 끝난 후, 활동 라인의 수를 줄일 때가 되었다.이 작업은 리소스 경쟁을 방지하기 위해 잠금 장치를 결합해야 합니다.
[_lock lock];
            _activeThreadCount--;
        }
}

지금 당신은 왜 순환의 맨 위에 자물쇠를 가져오는지 볼 수 있습니다.순환 중의 마지막 일은 활동 라인의 수를 줄이는 것이다. 이것은 잠긴 상태를 유지해야 한다.순환의 맨 위에 있는 첫 번째 일은 Block의 대기열을 검사하는 것입니다.순환 밖에서 첫 번째 자물쇠를 실행함으로써 후속으로 반복되는 모든 조작은 하나의 단일한 자물쇠로 조작할 수 있다. 둘째, 자물쇠, 잠금 해제, 그리고 자물쇠...
addBlock 방법:
    - (void)addBlock: (dispatch_block_t)block {

이곳의 모든 일은 자물쇠와 결합하여 얻어야 한다
        [_lock lock];

첫 번째 작업은 Block을 Block 대기열에 추가하는 것입니다.
        [_blocks addObject: block];

만약 이 Block을 가져갈 방치된 라인이 있다면, 그 다음에는 할 일이 없을 것이다.완료되지 않은 Block을 실행할 충분한 유휴 라인이 없고, 작업 라인의 수량이 상한선에 도달하지 않았다면, 새로운 라인을 만들 때가 되었습니다.
NSUInteger idleThreads = _threadCount - _activeThreadCount;
if([_blocks count] > idleThreads && _threadCount < _threadCountLimit) {
        [NSThread detachNewThreadSelector: @selector(workerThreadLoop:)
                                 toTarget: self
                               withObject: nil];
        _threadCount++;
}

현재 하나의 작업 라인이 시작되는 모든 준비 작업이 이미 끝났다.가령 그들 이 모두 잠든 상태 라고 가정한다면, 한 사람 을 깨워라
[_lock signal];

자물쇠를 풀면 완성입니다.
 [_lock unlock];    
} 

이것은 Block 서비스에 들어갈 수 있도록 미리 설정된 작업 라인을 생산하는 라인 탱크를 제공합니다.지금 이 대열을 위해 기초적인 실현을 하다.
대기열은 스레드 탱크처럼 이루어지고, 대기열은 자물쇠를 사용하여 그의 내용을 보호할 것이다.스레드 풀과 다른 점은 그가 어떤 기다림이나 신호를 보내는 동작도 하지 않고 기본적인 상호 배척만 하기 때문에 우리는 일반적인 NSLock을 사용한다.
NSLock *_lock;

스레드 풀처럼, 그는 마운트된 Block의 대기열을 유지하고, NSMutable Array를 사용합니다.
NSMutableArray *_pendingBlocks;

대기열은 이것이 직렬인지 병렬인지 알아야 합니다.
BOOL _serial;

이 값이 진짜라면, Block 온라인 채널에서 실행되는지 추적해야 합니다.
BOOL _serialRunning;

병렬 대기열은 작업이 실행되고 있든 없든 똑같기 때문에 추적하지 않습니다.
전역 대기열은 전역 변수로 저장되고, 밑바닥이 공유하는 스레드 풀도 마찬가지다.이들은 모두 +initialize 메서드에서 작성됩니다.
static MADispatchQueue *gGlobalQueue;
    static MAThreadPool *gThreadPool;

    + (void)initialize {
        if(self == [MADispatchQueue class]) {
            gGlobalQueue = [[MADispatchQueue alloc] initSerial: NO];
            gThreadPool = [[MAThreadPool alloc] init];
        }
    }

전역 대기열을 가져오는 방법은 이 변수만 되돌려줍니다. initialize 방법에서 그를 만들었는지 확인하기 때문입니다.
+ (MADispatchQueue *)globalQueue {
        return gGlobalQueue;
}

초기화 대기열은 분배 자물쇠, Block 대기열 끊기 및 설정Serial 변수
- (id)initSerial: (BOOL)serial {
        if ((self = [super init])) {
            _lock = [[NSLock alloc] init];
            _pendingBlocks = [[NSMutableArray alloc] init];
            _serial = serial;
        }
        return self;
}

나머지 공개 API와 접촉하기 전에, 온라인 채널에 단일한 Block을 보내고, 그 자신을 호출해서 다른 Block을 실행해야 하는 밑바닥 방법이 있습니다.
- (void)dispatchOneBlock {

이 방법의 목적은 온라인 채널에서 물건을 운행하는 것이기 때문에 그는 이곳에서 발송한다.
    [gThreadPool addBlock: ^{

그리고 그는 대열의 첫 번째 블록을 잡았다.자연히 이것은 반드시 자물쇠와 결합하여 완성해야 재난 사고를 피할 수 있다.
    [_lock lock];
    dispatch_block_t block = [_pendingBlocks firstObject];
    [_pendingBlocks removeObjectAtIndex: 0];
    [_lock unlock];

Block을 획득하고 자물쇠를 풀면 Block은 백엔드 라인에서 안전하게 실행할 수 있습니다
block();

만약 대열이 병렬적이라면, 이것은 모든 해야 할 일이다.이것이 Serial인 경우 다음이 필요합니다.
 if(_serial) {

직렬 대기열에 추가 블록을 만들 것입니다. 그러나 블록이 완성되기 전에 불러올 수 없습니다.Block이 완료되면 dispatch One Block은 대기열에 다른 끊긴 Block이 있는지 확인합니다. 만약 있다면 다음 Block을 보내도록 호출합니다. 그렇지 않으면 대기열의 운행 상태를 NO로 설정합니다.
                [_lock lock];
                if([_pendingBlocks count] > 0) {
                    [self dispatchOneBlock];
                } else {
                    _serialRunning = NO;
                }
                [_lock unlock];
            }
        }];
    }

이 방법으로 디스패치 Async를 실현하는 것은 상당히 간단하다.중단된 Block의 대기열에 Block을 추가하고 상태를 설정하고 상황에 따라 dispatchOneBlock을 불러옵니다.
- (void)dispatchAsync: (dispatch_block_t)block {
        [_lock lock];
        [_pendingBlocks addObject: block];

만약 직렬 대기열이 유휴 상태라면, 실행 상태로 설정하고 dispatchOneBlock을 호출해서 할 일을 실행합니다.
if(_serial && !_serialRunning) {
            _serialRunning = YES;
            [self dispatchOneBlock];

만약 대기열이 병렬적이라면, 무조건dispatchOneBlock을 호출합니다.이것은 새로운 Block이 가능한 한 빨리 실행될 수 있도록 보장합니다. 비록 다른 Block이 실행 중이지만, 동시에 여러 Blocks가 실행될 수 있기 때문입니다.
 } else if (!_serial) {
            [self dispatchOneBlock];
        }

만약 직렬이 이미 실행되었다면, 더 이상 할 일이 없을 것이다.dispatchOneBlock은 대기열에 추가된 모든 Block을 실행합니다.현재 잠금 해제:
    [_lock unlock];
    }

dispatchSync에서 GCD는 대기열의 다른 Block을 정지할 때 호출된 라인에서 Block을 직접 실행합니다. (만약 이것이 직렬이라면.)우리는 이렇게 스마트하게 하려고 시도하고 싶지 않다.대신, 우리는 디스패치 Async: 를 포장해서 그가 실행이 끝날 때까지 기다릴 수 있도록 합니다.그는 부분적인 NSCondition 변수를 사용하고 Done의 BOOL 변수를 추가하여 Block이 언제 완성되었는지 나타냅니다.
- (void)dispatchSync: (dispatch_block_t)block {
        NSCondition *condition = [[NSCondition alloc] init];
        __block BOOL done = NO;

그리고 그는 비동기적으로 Block을 보냈다.전송된 Block을 호출한 다음 완료 상태로 설정하고 조건 잠금 신호를 보냅니다.
 [self dispatchAsync: ^{
            block();
            [condition lock];
            done = YES;
            [condition signal];
            [condition unlock];
 }];

원래 호출 중인 라인으로 돌아가서, 우리가 해야 할 일은 대기 상태가done로 설정된 다음에 되돌아오는 것이다.
        [condition lock];
        while (!done) {
            [condition wait];
        }
        [condition unlock];
    }

여기까지 Block의 실행이 완료되었습니다. 이것도 MADispatchQueue의 API가 마지막으로 해야 할 일입니다.
결론적으로 전체적인 스레드 탱크는 하나의 작업 블록과 비교적 스마트한 스레드를 통해 실현될 수 있다.공유된 글로벌 스레드 풀을 사용하여 직렬/동시 및 동기/비동기식 디스패치를 위한 기본 디스패치 대기열을 제공하는 API를 만들 수 있습니다.이번 재건축은 많은 GCD의 훌륭한 특성이 결여되어 있고 매우 비효율적이다.어쨌든 이것은 우리로 하여금 내부 작업 원리를 잘 이해하게 했다.

좋은 웹페이지 즐겨찾기