Swift iOS - DispatchWorkItem все еще работает, хотя он отменяется и устанавливается на ноль

Я использую GCD DispatchWorkItem для отслеживания моих данных, которые отправляются в firebase.

Первое, что я делаю, это объявляю 2 свойства класса типа DispatchWorkItem, а затем, когда я готов отправить данные в firebase, я инициализирую их значениями.

Первое свойство называется errorTask. При инициализации он cancels firebaseTask и устанавливает его в nil, а затем печатает «errorTask fired». У него есть DispatchAsync Timer, который вызовет его через 0,0000000001 секунды, если errorTask не будет отменен до этого.

Второе свойство называется firebaseTask. При инициализации он содержит функцию, которая отправляет данные в firebase. Если обратный вызов firebase выполнен успешно, то errorTask отменяется и устанавливается на nil, а затем печатается оператор печати «обратный вызов firebase был достигнут». Я также проверяю, была ли отменена задача firebaseTask.

Проблема в том, что код внутри errorTask всегда запускается до того, как будет достигнут обратный вызов firebaseTask. Код errorTask отменяет firebaseTask и устанавливает его в ноль, но по какой-то причине firebaseTask все еще работает. Я не могу понять, почему?

Операторы печати поддерживают тот факт, что errorTask запускается первым, потому что "errorTask fired" всегда печатается до "firebase callback was reached".

Почему firebaseTask не отменяется и не устанавливается на nil, даже если это происходит из-за errorTask?

В моем фактическом приложении происходит следующее: если пользователь отправляет некоторые данные в Firebase, появляется индикатор активности. После достижения обратного вызова firebase индикатор активности отключается, и пользователю отображается предупреждение о том, что он был успешным. Однако, если индикатор активности не имеет таймера и обратный вызов никогда не достигается, он будет вращаться вечно. DispatchAsyc after имеет таймер, установленный на 15 секунд, и если обратный вызов не будет достигнут, появится метка ошибки. 9 из 10 раз всегда работает.

  1. отправить данные в фб
  2. показать индикатор активности
  3. обратный вызов достигнут, поэтому отмените errorTask, установите для него значение nil и отключите индикатор активности
  4. показать предупреждение об успехе.

Но время от времени

  1. это займет больше времени, чем 15 секунд
  2. firebaseTask отменяется и устанавливается равным нулю, а индикатор активности будет закрыт.
  3. метка ошибки будет отображаться
  4. предупреждение об успехе все равно появится

Блок кода errorTask отклоняет actiInd, показывает errorLabel, отменяет firebaseTask и устанавливает для него значение nil. После того как firebaseTask будет отменен и установлен в nil, я предположил, что все внутри него остановится еще и потому, что обратный вызов так и не был достигнут. Это может быть причиной моего замешательства. Кажется, как будто даже несмотря на то, что firebaseTask отменено и установлено на ноль, someRef?.updateChildValues(... каким-то образом все еще работает, и мне также нужно отменить это.

Мой код:

var errorTask:DispatchWorkItem?
var firebaseTask:DispatchWorkItem?

@IBAction func buttonPush(_ sender: UIButton) {

    // 1. initialize the errorTask to cancel the firebaseTask and set it to nil
    errorTask = DispatchWorkItem{ [weak self] in
        self?.firebaseTask?.cancel()
        self?.firebaseTask = nil
        print("errorTask fired")
        // present alert that there is a problem
    }

    // 2. if the errorTask isn't cancelled in 0.0000000001 seconds then run the code inside of it
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.0000000001, execute: self.errorTask!)

    // 3. initialize the firebaseTask with the function to send the data to firebase
    firebaseTask = DispatchWorkItem{ [weak self]  in

        // 4. Check to see the if firebaseTask was cancelled and if it wasn't then run the code
        if self?.firebaseTask?.isCancelled != true{
            self?.sendDataToFirebase()
        }

       // I also tried it WITHOUT using "if firebaseTask?.isCancelled... but the same thing happens
    }

    // 5. immediately perform the firebaseTask
    firebaseTask?.perform()
}

func sendDataToFirebase(){

    let someRef = Database.database().reference().child("someRef")

    someRef?.updateChildValues(myDict(), withCompletionBlock: {
        (error, ref) in

        // 6. if the callback to firebase is successful then cancel the errorTask and set it to nil
        self.errorTask?.cancel()
        self.errorTask? = nil

        print("firebase callback was reached")
    })

}

person Lance Samaria    schedule 17.02.2018    source источник


Ответы (1)


Эта процедура отмены не делает того, что, как я подозреваю, вы думаете. При отмене DispatchWorkItem упреждающая отмена не выполняется. Это, конечно, не имеет никакого отношения к вызову updateChildValues. Все, что он делает, — это выполняет поточно-безопасную настройку свойства isCancelled, которую, если вы вручную выполняете итерацию в цикле, вы можете периодически проверять и преждевременно выходить, если видите, что задача была отменена.

В результате проверка isCancelled в начале задачи не очень полезная схема, потому что, если задача еще не создана, отменять нечего. Или, если задача была создана и добавлена ​​в очередь и отменена до того, как очередь успела запуститься, очевидно, что она просто будет отменена, но никогда не будет запущена, и вы никогда не доберетесь до своего isCancelled теста. И если задача запущена, она, скорее всего, прошла тест isCancelled до вызова cancel.

Суть в том, что попытки рассчитать время запроса cancel так, чтобы он был получен точно после запуска задачи, но до того, как он дошел до теста isCancelled, будут бесполезными. У вас есть гонка, которую будет почти невозможно точно рассчитать. Кроме того, даже если вам удалось правильно рассчитать время, это просто демонстрирует, насколько неэффективен весь этот процесс (только 1 из миллиона cancel запросов сделает то, что вы намеревались).

Как правило, если у вас есть асинхронная задача, которую вы хотите отменить, вы должны обернуть ее в асинхронный пользовательский подкласс Operation и реализовать метод cancel, который останавливает базовую задачу. Очереди операций просто предлагают более изящные шаблоны для отмены асинхронных задач, чем очереди отправки. Но все это предполагает, что базовая асинхронная задача предлагает механизм для ее отмены, и я не знаю, предлагает ли вообще Firebase значимый механизм для этого. Я, конечно, не видел, чтобы это рассматривалось ни в одном из их примеров. Так что все это может быть спорным.

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


Кроме того, в вашем примере есть и другие технические проблемы.

В частности, я предполагаю, что вы запускаете это в основной очереди. Поэтому task.perform() немедленно запускает его в текущей очереди. Но ваш DispatchQueue.main.asyncAfter(...) может быть запущен только тогда, когда все, что выполняется в основной очереди, выполнено. Таким образом, даже если вы указали задержку в 0,0000000001 секунды, она на самом деле не будет работать до тех пор, пока не будет доступна основная очередь (а именно, после того, как ваш perform завершит работу в основной очереди, и вы пройдете тест isCancelled).

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

weak var task: DispatchWorkItem?

let item = DispatchWorkItem {
    if (task?.isCancelled ?? true) {
        print("canceled")
    } else {
        print("not canceled in time")
    }
}

DispatchQueue.global().asyncAfter(deadline: .now() + 0.00001) {
    task?.cancel()
}

task = item
DispatchQueue.main.async {
    item.perform()
}

Теперь вы можете поиграть с различными задержками и увидеть различное поведение между задержкой в ​​0,1 секунды и задержкой в ​​0,0000000001 секунды. И вы захотите убедиться, что приложение достигло состояния покоя, прежде чем пытаться выполнить этот тест (например, сделайте это в событии нажатия кнопки, а не в viewDidLoad).

Но опять же, это просто проиллюстрирует тщетность всего упражнения. Вам будет очень трудно поймать задачу между моментом ее запуска и до того, как она проверит свойство isCancelled. Если вы действительно хотите воспроизвести логику отмены каким-то повторяющимся образом, нам придется сделать это искусственно:

weak var task: DispatchWorkItem?

let queue = DispatchQueue(label: "com.domain.app.queue") // create a queue for our test, as we never want to block the main thread

let semaphore = DispatchSemaphore(value: 0)

let item = DispatchWorkItem {
    // You'd never do this in a real app, but let's introduce a delay
    // long enough to catch the `cancel` between the time the task started.
    //
    // You could sleep for some interval, or we can introduce a semphore
    // to have it not proceed until we send a signal.

    print("starting")
    semaphore.wait() // wait for a signal before proceeding

    // now let's test if it is cancelled or not

    if (task?.isCancelled ?? true) {
        print("canceled")
    } else {
        print("not canceled in time")
    }
}

DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
    task?.cancel()
    semaphore.signal()
}

task = item
queue.async {
    item.perform()
}

Вы бы никогда этого не сделали, но это просто показывает, что isCancelled действительно работает.

Честно говоря, вы бы никогда не использовали isCancelled таким образом. Обычно вы используете процесс isCancelled, если выполняете какой-то длительный процесс, в котором вы можете периодически проверять статус isCancelled и выходить, если он верен. Но в вашей ситуации это не так.

Вывод из всего этого состоит в том, что проверка isCancelled в начале задачи вряд ли когда-нибудь даст то, на что вы надеялись.

person Rob    schedule 17.02.2018
comment
Спасибо за помощь. Я использовал WorkItems, потому что я читал, что они были эквивалентом OperationQueues в GCD. Я столкнулся с isCancelled и поэтому использовал его. Даже без его использования возникает та же проблема. Что я хочу сделать, так это то, что если пользователь публикует изображение, показывается действие, данные изображения сначала отправляются в хранилище, а затем URL-адрес переходит в базу данных. Поскольку есть 2 шага, все может пойти не так. Если все хорошо, предупредите пользователя. Если это займет больше времени, чем x, предупредите пользователя о повторной попытке. Он был установлен на 15 секунд, и иногда предупреждение отображалось для обоих. Операторы печати == Оповещения - person Lance Samaria; 18.02.2018
comment
В моем реальном приложении, если это занимает слишком много времени, я отклоняю действие Ind и показываю метку ошибки. В случае успеха я показываю предупреждение. Это была просто более короткая версия, поэтому есть только 1 попытка. Если errorTask не отменяется в течение 0...1 секунды, это означает, что обратный вызов в sendDataToFirebase() никогда не был достигнут, а self.errorTask?.cancel() и self.errorTask? = nil никогда не запускался. Если они никогда не запускаются, то errorTask должен отменить firebaseTask, а если он отменен и установлен на ноль, то все, что внутри него, также должно быть закрыто. Вот почему я сделал это таким образом. - person Lance Samaria; 18.02.2018
comment
Вот откуда я получил isCancelled. Я только что понял, что это твой ответ. stackoverflow.com/a/38372384/4833705 Но даже когда я использовал: firebaseTask = DispatchWorkItem{ [weak self] в себе? .sendDataToFirebase() } без проверки isCancelled Я столкнулся с той же проблемой. - person Lance Samaria; 18.02.2018
comment
@LanceSamaria - В этом ответе мы делаем медленную работу и неоднократно проверяем, isCancelled ли это на протяжении всего процесса. Так что смысл есть. (И, тем не менее, очереди операций по-прежнему предлагают более широкий набор функций отмены, чем GCD.) Что не имеет никакого смысла, так это проверять один и только один раз и где начинается DispatchWorkItem. И если Firebase не предоставит какой-либо API для изящной отмены updateChildValues, что-то, что вы инициируете, когда отменяете задачу/операцию, я все равно не уверен, какова цель всей этой отмены. - person Rob; 18.02.2018
comment
Вот что прояснит это для меня. updateChildValues ​​запускается внутри firebaseTask. Если firebaseTask запущен и выполняется updateChildValues, предполагая, что его обратный вызов никогда не будет достигнут за xxx секунд, если я отменю firebaseTask и установлю для него значение nil, что произойдет с updateChildValues? Он все еще работает? Если да, то что произойдет, если он подключится или не подключится? Звучит так, как вы говорите, хотя я отменяю firebaseTask и устанавливаю для него значение nil, я не отменяю updateChildValues, потому что он уже активен - person Lance Samaria; 18.02.2018
comment
Ага, до сих пор бегает. В конечном итоге он, несомненно, объявит о своем закрытии, либо сообщив об успехе, либо о превышении времени в соответствии со своими собственными критериями. Установка состояния DispatchWorkItem не имеет к этому никакого отношения. (Извините, я должен был сделать это более явным в своей критике выше, поскольку это довольно важное наблюдение.) Вы можете отменить его, только если API предоставляет какой-либо метод отмены, уникальный для конкретного рассматриваемого API. Некоторые асинхронные API предлагают свои собственные методы отмены (например, URLSession или CLGeocoder), а другие — нет. - person Rob; 18.02.2018
comment
Большое спасибо за ясность. Теперь я понимаю. Не могли бы вы указать это где-нибудь в своем ответе, чтобы я мог принять его. Ваша другая информация была хорошей, но для меня главное, что updateChildVales все еще работал, даже несмотря на то, что firebaseTask был отменен и установлен на ноль. Я не уверен, многие ли это осознают. Еще раз большое спасибо ????. Теперь я иду искать способ отменить updateChildValues - person Lance Samaria; 18.02.2018
comment
Обновленный ответ соответственно. - person Rob; 18.02.2018
comment
Спасибо за разъяснения! ???????????????? - person Lance Samaria; 18.02.2018
comment
Обычно вы используете процесс isCancelled, если выполняете какой-то длительный процесс, в котором вы можете периодически проверять статус isCancelled и выходить, если он верен. Можете ли вы поделиться реальными сценариями, где это полезно? например загрузка файла размером 1 ГБ может занять некоторое время, но это не то, для чего у меня был бы цикл for. Может быть, перемещение 1000 файлов? Где я могу перебирать каждый элемент? Что-нибудь еще? - person Honey; 02.10.2019
comment
Распространенным примером являются операции с интенсивными вычислениями, такие как процедуры обработки изображений, когда вы выполняете итерацию по пиксельному буферу, пиксель за пикселем, выполняя некоторые вычисления. Или, возможно, вычисление множества Мандельброта, или вычисление π, или что-то в этом роде. - person Rob; 02.10.2019
comment
Спасибо. Я не делаю ничего из этого. Не слышал о некоторых из них: D Думаю, это не очень полезно для меня. Я должен просто использовать Timer. - person Honey; 02.10.2019
comment
@ Роб, ты написал какие-нибудь книги? я хочу взять один. - person BigFire; 18.08.2020