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

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

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

Первое свойство называется errorTask, Когда инициализировал это cancels firebaseTask и устанавливает его nil затем печатает "Ошибка выполнения задачи". Оно имеет DispatchAsync Timer это вызовет его через 0.0000000001 секунд, если errorTask не был отменен ранее.

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

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

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

Как получается, что firebaseTask не отменяется и не устанавливается в ноль, хотя errorTask заставляет все это происходить?

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

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

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

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

errorTask Блок кода отменяет действие, показывает errorLabel и отменяет firebaseTask и устанавливает его на ноль. Как только firebaseTask отменен и установлен в ноль, я предполагал, что все в нем остановится также, потому что обратный вызов никогда не был достигнут. Это может быть причиной моего замешательства. Кажется, будто 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")
    })

}

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 в начале задания вряд ли когда-либо будет достигнуто то, на что вы рассчитывали.

Другие вопросы по тегам