повторное использование dispatch_semaphore_t — Что мне здесь не хватает?

У меня есть код, в котором я использую dispatch_semaphore_t, чтобы сигнализировать о завершении операции. Когда семафор является переменной-членом, кажется, что он ведет себя неправильно. Я покажу пример кода, который работает, и пример, который, кажется, не работает:

@implementation someClass  
{  
  dispatch_semaphore_t memberSem;  
  dispatch_semaphore_t* semPtr;  
  NSThread* worker;
  BOOL taskDone;  
}  

- (id)init  
{  
  // Set up the worker thread and launch it - not shown here.  
  memberSem= dispatch_semaphore_create(0); 
  semPtr= NULL;  
  taskDone= FALSE;  
}  

- (void)dealloc  
{  
  // Clean up the worker thread as needed - not shown here.  
  if((NULL != semPtr) && (NULL != *semPtr))  
    disptatch_release(*semPtr);  

  dispatch_release(memberSem);  
}  

- (void)doSomethingArduous  
{  
  while([self notDone])  // Does something like check a limit.  
    [self doIt];  // Does something like process data and increment a counter.  

  taskDone= TRUE;  // I know this should be protected, but keeping the example simple for now.  

  if((NULL != semPtr) && (NULL != *semPtr))  
    dispatch_semaphore_signal(*semPtr);  // I will put a breakpoint here, call it  "SIGNAL"  
}  

- (BOOL)getSomethingDoneUseLocalSemaphore  
{  
  taskDone= FALSE;  // I know this should be protected, but keeping the example simple for now.  
  dispatch_semaphore_t localSem= dispatch_semaphore_create(0);  
  semPtr= &localSem;  
  [self performSelector:doSomethingArduous onThread:worker withObject:nil waitUntilDone:NO];  

  dispatch_time_t timeUp= dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(2.5 * NSEC_PER_SEC));  
  dispatch_semaphore_wait(localSem, timeUp);  

  semPtr= NULL;  
  dispatch_release(localSem);  

  // I know I could just return taskDone. The example is this way to show what the problem is.  
  if(taskDone)  // Again with thread safety.  
    return TRUE;    

  return FALSE;  
}  

- (BOOL)getSomethingDoneUseMemberSemaphore  
{  
  taskDone= FALSE;  // I know this should be protected, but keeping the example simple for now.  

  semPtr= &memberSem;  // I will put a breakpoint here, call it "START"  
  [self performSelector:doSomethingArduous onThread:worker withObject:nil waitUntilDone:NO];  

  dispatch_time_t timeUp= dispatch_time(DISPATCH_TIME_NOW, (uint64_t)(2.5 * NSEC_PER_SEC));  
  dispatch_semaphore_wait(memberSem, timeUp);  

  semPtr= NULL;  

  // I know I could just return taskDone. The example is this way to show what the problem is.  
  if(taskDone)  // Again with thread safety.  
    return TRUE;  // I will put a breakpoint here, call it "TASK_DONE"   

  return FALSE;  // I will put a breakpoint here, call it "TASK_NOT_DONE"  
}  

- (void)hereIsWhereWeBringItTogether  
{  
  BOOL gotItDoneLocal= [self getSomethingDoneUseLocalSemaphore];  // Will return TRUE.  
  gotItDoneLocal= [self getSomethingDoneUseLocalSemaphore];  // Will return TRUE.  
  gotItDoneLocal= [self getSomethingDoneUseLocalSemaphore];  // Will return TRUE.  

  BOOL gotItDoneMember= [self getSomethingDoneUseMemberSemaphore];  // Will return TRUE. I will put a breakpoint here, call it "RUN_TEST"  
  gotItDoneMember= [self getSomethingDoneUseMemberSemaphore];  // Will return FALSE.  
}  

Итак, учитывая этот код и результаты, которые я получаю/получаю, я ставлю точки останова, как описано в моем реальном коде: одну в основной функции, одну для запуска в рабочей функции, одну, где сигнализируется семафор-член, и две после ждать.

То, что я обнаружил, было в случае, когда я использую семафор члена, в первом раунде я останавливаюсь в точке останова «RUN_TEST», запускаю и нажимаю точку останова «START», запускаю, затем нажимаю точку останова «SIGNAL», запускаю, затем нажимаю точку останова «TASK_DONE» - все как положено.

Когда я продолжаю работать, я нажимаю точку останова «START», запускаю, затем нажимаю точку останова «TASK_NOT_DONE», запускаю, затем нажимаю точку останова «SIGNAL».

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

Кажется, я либо не управляю правом подсчета (пары сигнал/ожидание), либо этот семафор-член не вернется в несигнальное состояние.

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

РЕДАКТИРОВАТЬ: В конечном счете, то, что мне, казалось, не хватало, было связано с тем, что мой фактический код был немного сложнее. Вместо чистого возврата из сложной задачи задействовано несколько потоков и пост-уведомление. Я заменил postNotification на код в обработчике уведомлений — он устанавливает флаг и сигнализирует семафору. Таким образом устраняется любая задержка, которая могла быть вызвана обработчиком уведомлений.


person GTAE86    schedule 21.08.2013    source источник


Ответы (3)


Да, это ожидаемое поведение. Если вы превысите время ожидания сигнала, когда сигнал придет, он будет перехвачен следующим вызовом dispatch_semaphore_wait для этого конкретного семафора. Рассмотрим следующий пример:

Например:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_time_t timeout;

// in 5 seconds, issue signal

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(5);
    NSLog(@"Signal 1");
    dispatch_semaphore_signal(semaphore);
});

// wait four seconds for signal (i.e. we're going to time out before the signal)

NSLog(@"Waiting 1");
timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC));
if (dispatch_semaphore_wait(semaphore, timeout))
    NSLog(@"Waiting for 1: timed out");
else
    NSLog(@"Waiting for 1: caught signal");

// now, let's issue a second signal in another five seconds

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(5);
    NSLog(@"Signal 2");
    dispatch_semaphore_signal(semaphore);
});

// wait another four seconds for signal

// this time we're not going to time out waiting for the second signal, 
// because we'll actually catch that first signal, "signal 1")

NSLog(@"Waiting 2");
timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC));
if (dispatch_semaphore_wait(semaphore, timeout))
    NSLog(@"Waiting for 2: timed out");
else
    NSLog(@"Waiting for 2: caught signal");

// note, "signal 2" is still forthcoming and the above code's 
// signals and waits are unbalanced 

Итак, когда вы используете переменную экземпляра класса, ваш getSomethingDoneUseMemberSemaphore ведет себя так, как указано выше, где второй вызов dispatch_semaphore_wait перехватит первый выданный сигнал, потому что (а) это тот же семафор; и (b) если время ожидания первого вызова dispatch_semaphore_signal истекло.

Но если вы каждый раз используете уникальные семафоры, то второй вызов dispatch_semaphore_wait не будет реагировать на dispatch_semaphore_signal первого семафора.

person Rob    schedule 21.08.2013
comment
Я понимаю что ты имеешь ввиду. По сути, если задача занимает больше времени, чем ожидание, время ожидания истечет, но сигнал по-прежнему будет увеличивать счетчик, поэтому следующее ожидание сработает немедленно. Я думал об этом, но в этом случае я могу подключить анализатор и увидеть, что действие выполняется менее чем за секунду. Думаю, я мог бы проверить, сигнализируется ли семафор во второй раз, просто выполнив ожидание перед запуском моего длинного процесса. Если время ожидания не истекло, я знаю, что происходит, и могу соответствующим образом настроить время ожидания. - person GTAE86; 22.08.2013
comment
@GTAE86 Верно. Кстати, то, как вы настроили свой локальный код семафора, теоретически вы могли бы увидеть то же поведение, что и пример вашей переменной-члена (это полностью вопрос точного времени). Если вы абсолютно не хотите взаимных помех между различными сигналами различных задач, а не выбираете семафор из какой-либо переменной экземпляра (даже ваш текущий пример локальной переменной в конечном итоге сохраняет семафор в переменной экземпляра), вы хотите передать семафор в качестве параметра, не используя переменную экземпляра (или создайте отдельный экземпляр вашего объекта для каждой операции). - person Rob; 22.08.2013
comment
Думаю, я могу увидеть, что происходит в моем коде. В игре задействовано несколько потоков (намного сложнее, чем в примере). Сложный процесс: в потоке A вызовите поток B для записи на устройство, подождите. Поток C получает ответ (ReadPipeAsync), кэширует его в очереди. Thread D отрывает его для анализа. Если хорошо, он публикует уведомление с результатом. Обработчик уведомлений выполняет некоторую проверку, устанавливает статус завершения (переменную, защищенную блокировкой) и сигнализирует семафору, если он запрошен. Хотя я знаю, что моя операция завершилась быстро, это уведомление может быть задержано. - person GTAE86; 22.08.2013
comment
Я понимаю, что вы имеете в виду, когда говорите об идентификаторе переменной. На самом деле я задавал этот вопрос, потому что надеялся избежать отправки семафора в селекторе! В основном это связано с логистикой в ​​коде, и эти проблемы, вероятно, можно решить. Я не был уверен, как передать семафор - можно ли его просто засунуть в PerformSelector:onThread:withObject:waitUntilDone: как объект? - person GTAE86; 22.08.2013
comment
@ GTAE86 Вызовы GCD в моем примере также заканчиваются отдельными рабочими потоками (это гораздо более простой способ создания потоков), но я понимаю вашу точку зрения. Хотя, похоже, ты попал в ловушку. - person Rob; 22.08.2013
comment
@ GTAE86 Возможность передачи семафора в качестве объекта может зависеть от того, является ли ваша минимальная цель iOS 6.0 или более ранней версией. (Эффективно iOS 6, объекты GCD являются объектами.) Я мог бы захотеть создать собственный класс с одним свойством, dispatch_semaphore_t, и передать этот объект. - person Rob; 22.08.2013
comment
Я думала об этом. Однако на данный момент мое приложение предназначено только для OS X, и я нацелен на 10.8 с API, который выполняет всю работу. - person GTAE86; 22.08.2013
comment
Похоже, мне все-таки придется его обернуть — компилятор говорит, что dispatch_semaphore_t — это struct dispatch_semaphore_s* - person GTAE86; 22.08.2013

Когда вы вызываете dispatch_semaphore_wait с тайм-аутом, а поток по-прежнему заблокирован на момент тайм-аута, происходит почти то же самое, как если бы был вызван dispatch_semaphore_signal. Одно отличие состоит в том, что dispatch_semaphore_signal активирует любой поток, но этот конкретный поток активируется по тайм-ауту. Другое отличие состоит в том, что dispatch_semaphore_wait вернет ненулевое значение вместо 0.

Вот в чем проблема: тот, кто собирался вызвать dispatch_semaphore_signal, все равно будет вызывать его, и тогда у нас будет на один сигнал слишком много. Этого может быть трудно избежать; если у вас есть 10-секундный тайм-аут, то dispatch_semaphore_signal может быть вызван через 10.000000001 секунд. Поэтому, если вы повторно используете семафор, у вас есть проблема.

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

Резюме: не используйте повторно семафор, если вы ожидаете его с тайм-аутом.

person gnasher729    schedule 04.06.2015
comment
Верно. В итоге я создал класс-держатель семафора, и когда я запускаю операцию, я создаю его экземпляр, настраиваю семафор и помещаю его в потокобезопасную коллекцию, принадлежащую классу. Таким образом, код завершения, работающий в другом потоке, может проверить его и сообщить, если он существует. Основная проблема, с которой я столкнулся в приведенном выше случае, заключалась в том, что я сигнализировал семафору в обработчике уведомлений: это приводило к задержке, из-за которой я выходил из ожидания. Итак, две вещи, которые я делал, привели к проблеме. Спасибо за отзыв! - person GTAE86; 09.07.2015

Я смог написать что-то похожее на то, что, как мне кажется, вы ищете, и, похоже, оно работает так, как вы этого хотите (но опять же, я не уверен на 100%, что понимаю, что вы ищете.):

ArduousTaskDoer.m

@implementation ArduousTaskDoer
{
    dispatch_semaphore_t mSemaphore;
    BOOL mWorkInProgress;
}

- (id)init
{
    if (self = [super init])
    {
        mSemaphore = dispatch_semaphore_create(0);
    }
    return self;
}

- (void)dealloc
{
    mSemaphore = nil;
}

- (void)doWork
{
    @synchronized(self)
    {
        mWorkInProgress = YES;
    }

    // Do stuff
    sleep(10);

    @synchronized(self)
    {
        mWorkInProgress = NO;
    }

    dispatch_semaphore_signal(mSemaphore);
}

- (BOOL)workIsDone
{

    @synchronized(self)
    {
        if (!mWorkInProgress)
        {
            mWorkInProgress = YES;
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [self doWork];
            });
        }
    }


    if (dispatch_semaphore_wait(mSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)2.5 * NSEC_PER_SEC)))
    {
        return NO;
    }

    return YES;
}

@end

...и затем код вызова:

ArduousTaskDoer* task = [[ArduousTaskDoer alloc] init];
BOOL isDone = NO;
while(!(isDone = [task workIsDone]))
{
    NSLog(@"Work not done");
}

NSLog(@"Work is done");

// Do it again... Semaphore is being reused
while(!(isDone = [task workIsDone]))
{
    NSLog(@"Work not done");
}

NSLog(@"Work is done");

Надеюсь это поможет.

person ipmcc    schedule 21.08.2013
comment
@impcc - Спасибо за ответ. Извините, но я запутался - мой отладчик сообщает, что dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.5 * NSEC_PER_SEC)) возвращает то же значение, что и dispatch_time(DISPATCH_TIME_NOW, (int64_t)2.5 * NSEC_PER_SEC). Я что-то упускаю? Что касается вашего понимания того, что я пытаюсь сделать - вы правы. Я просто пытаюсь дождаться в одном потоке выполнения задачи в другом потоке. В случае вашего примера похоже, что вы ждете 2,5 секунды для завершения 10-секундной задачи, поэтому сигнал будет поступать после истечения ожидания. Правильный? - person GTAE86; 22.08.2013
comment
Понятно... Я неправильно прочитал ваш код и подумал, что вы просто передаете (2.5 * NSEC_PER_SEC) непосредственно dispatch_semaphore_wait. Виноват. Да, сигнал появится после первого ожидания. Если вы вызываете функцию ожидания более одного раза, она будет ждать 2,5 секунды при каждом вызове. Если вы просто хотите дождаться полного завершения задачи, просто используйте DISPATCH_TIME_FOREVER. С другой стороны, если вы хотите асинхронно получать уведомления о завершении задачи, почему бы не использовать dispatch_group_notify? - person ipmcc; 22.08.2013
comment
В основном потому, что я на самом деле не использую GCD, а только семафоры. Из того, что я читал, я могу нарушать всевозможные правила, делая это, но, похоже, это работает очень хорошо. Я прочитал все руководства Apple по параллелизму и многопоточности и знаю, что они рекомендуют не использовать потоки, если это возможно. В моем случае я должен запускать открытые сеансы ввода-вывода с несколькими чипами - в основном говорю чипам, чтобы они начали собирать данные и обрабатывать их, пока я не скажу им остановиться. Реальные пользователи могут реально запускать его часами, поэтому потоки команд/управления и сбора данных могут выполняться долгое время. - person GTAE86; 22.08.2013