В Objective- C при реализации метода, который вызывает обработчик завершения после завершения фоновой задачи, как определить безопасность потоков? - PullRequest
1 голос
/ 21 июня 2020

Недавно я изменил существующий проект и изменил некоторые методы, возвращающие значение (BOOL) для вызова блока завершения. Я последовал за этим очень хорошим ответом:

{ ссылка }

Теперь у меня есть метод, который вызывает блок завершения, он отлично работает, но я боюсь, что может непредсказуемо потерпит неудачу из-за того, что не является потокобезопасным.

У меня есть объявление блока:

typedef void(^myCompletion)(id, BOOL);

Ниже приведен мой метод с обработчиком завершения:

-(void)myMethodThatTakesAnArgument:(id)object completionHandler:(myCompletion)completionblock {
    //do things that are allowed in the main thread:
    //...
    //...
    dispatch_queue_t backgroundQueue =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_async(backgroundQueue, ^{

        [self doWorkButReturnImmediately];
        BOOL workIsNotFinished = [self isJobFinished];

        NSDate *processingStart = [NSDate date];
        NSTimeInterval timeElapsed = 0.0;
        while (workIsNotFinished && timeElapsed < 15.0) {
            [NSThread sleepForTimeInterval:1.0];
            timeElapsed = [[NSDate date] timeIntervalSinceDate:processingStart];
            workIsNotFinished = [self isJobFinished];
        }

      dispatch_async(dispatch_get_main_queue(), ^{
          // Return Result
          completionblock(object,YES);
      });
  });   
}

Ниже приводится мой метод вызывающего абонента:

- (void)callerMethod {

    NSArray *arrayOfObjects = [self prepareArrayOfObjects];

    NSMutableArray *unprocessedObjects = [arrayOfObjects mutableCopy];

    for (NSString *string in arrayOfObjects) {

        [self myMethod:string completionblock:^(id obj, BOOL success) {
        
        [unprocessedObjects removeObject:string];
        if(success){
            // do something with obj
            NSLog(@"success");
        }
        
        if ([unprocessedObjects count] == 0) {
        
            dispatch_async(dispatch_get_main_queue(), ^{
            
                // Everything is done, act accordingly.
            
            });
        
            }
        
        }
  }
}

Я подозреваю, что этот сценарий может каким-то образом потерпеть неудачу, и я думаю добавить некоторый код безопасности потоков. Я не очень разбираюсь в этом предмете, но мне кажется, что, вероятно, @synchronized может быть путем к go. Поэтому я хотел бы встроить код вызываемого метода в некоторые операторы @synchronized, но я не уверен, нужно ли это в данном случае, и, если это так, я не уверен, где конкретно разместить операторы, я думаю, вероятно, в той части, которая отправляется в фоновой очереди. В код я включил простой объект, который передается обратно в обработчик завершения, который может действовать как объект, передаваемый в @synchronized. Любая помощь приветствуется. Спасибо

1 Ответ

3 голосов
/ 21 июня 2020

Единственная проблема с потокобезопасностью, которая бросается мне в глаза, - это isJobFinished. Вы вызываете его неоднократно из второго потока, поэтому его реализация должна быть потокобезопасной.

Однако это НЕ способ проверки завершения. Просто запомните это правило: плохой опрос. Семафоры хороши.

Это намного более чистая и эффективная реализация, позволяющая избежать проблем с isJobFinished:

{
    dispatch_semaphore_t jobFinishedSemaphore = dispatch_semaphore_create(0);
    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    dispatch_async(backgroundQueue, ^{
        [self doWorkSignalingWhenDone:jobFinishedSemaphore];
        
        if ( dispatch_semaphore_wait(jobFinishedSemaphore, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC)) ) {
            // work timed out
        } else {
            // work successfully completed
            dispatch_async(dispatch_get_main_queue(), ^{
                // Return Result
                completionblock(self,YES);
            });
        }
    });
}

- (void)doWorkSignalingWhenDone:(dispatch_semaphore_t)finishedSemaphore
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
        // do some work in the background
        // ...
        // signal work is finished
        dispatch_semaphore_signal(finishedSemaphore);
    });
}

Если изменение doWorkButReturnImmediately нецелесообразно, но isJobFinished является наблюдаемым, вы можете сделать что-то подобное, используя наблюдатель значений. В методе наблюдателя сигнализируйте семафор, когда значение isJobFinished изменяется с NO на YES.

Лучшее решение для передачи jobFinishedSemaphore методу наблюдателя - сделать его экземпляром переменная объекта. Однако это означает, что вы можете одновременно выполнять только одну из этих задач. Если у вас должно быть несколько заданий в полете одновременно, или вы просто не можете редактировать переменные в классе, это должно сработать:

{
    dispatch_semaphore_t jobFinishedSemaphore = dispatch_semaphore_create(0);
    dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

    [self addObserver:self
           forKeyPath:@"isJobFinished"
              options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
              context:(__bridge void * _Nullable)(jobFinishedSemaphore)];   /* WARNING! not retained */

    dispatch_async(backgroundQueue, ^{
        [self doWorkButReturnImmediately];
        
        if ( dispatch_semaphore_wait(jobFinishedSemaphore, dispatch_time(DISPATCH_TIME_NOW, 15*NSEC_PER_SEC)) ) {
            // work timed out
        } else {
            // work successfully completed
            dispatch_async(dispatch_get_main_queue(), ^{
                // Return Result
                completionblock(object,YES);
            });
        }
        
        // Note: this is "mostly" thread safe because this block still retains a reference to
        //  jobFinishedSemaphore, so that semephore has not been destroyed yet.
        // Remove the value observer before the reference to jobFinishedSemaphore goes out of scope.
        [self removeObserver:self forKeyPath:@"isJobFinished" context:(__bridge void * _Nullable)(jobFinishedSemaphore)];
        // at this point jobFinishedSemaphore goes out of scope, but the observer has been removed so it
        //  should no longer get sent with the (now invalid void*) reference to jobFinishedSemaphore.
    });
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context
{
    if ([keyPath isEqualToString:@"isJobFinished"])
        {
        if (   ![change[NSKeyValueChangeOldKey] boolValue]  // value was NO
            && [change[NSKeyValueChangeNewKey] boolValue] ) // value now YES
            {
            dispatch_semaphore_signal((__bridge dispatch_semaphore_t _Nonnull)(context));
            }
        }
}

Страшная часть этого кода заключается в том, что вы ' повторная передача несохраняемой копии указателя семафора наблюдателю. Но если вы удалите наблюдателя до того, как семафор будет уничтожен, все будет в порядке.

...