Swift iOS -DispatchWorkItem仍在运行,即使它已取消并设置为Nil

Lan*_*ria 3 grand-central-dispatch ios swift dispatchworkitem

我使用GCD DispatchWorkItem来跟踪我发送到firebase的数据.

我做的第一件事是声明类型的2个类属性,DispatchWorkItem然后当我准备好将数据发送到firebase时,我用值初始化它们.

第一个属性被命名errorTask.当初始化它cancelsfirebaseTask,并将它设置nil,然后打印"errorTask炒鱿鱼".它有一个DispatchAsync Timer,将调用它0.0000000001秒如果errorTask没有之前那么取消.

第二个属性被命名firebaseTask.初始化时,它包含一个将数据发送到firebase的函数.如果firebase回调成功,则errorTask取消并设置为nil,然后打印一个打印语句"firebase callback".我还检查firebaseTask是否被取消.

问题是在到达回调errorTask之前始终运行的代码firebaseTask.该errorTask代码取消firebaseTask,将其设为零,但由于某种原因,firebaseTask仍然运行.我想不通为什么?

print语句支持errorTask首先运行的事实,因为 "errorTask fired"之前总是打印出来"firebase callback was reached".

为什么firebaseTask没有被取消并设置为nil,即使errorTask让这些事情发生了?

在我的实际应用程序中,如果用户向Firebase发送一些数据,则会出现活动指示符.达到firebase回调后,活动指示器将被解除,并向用户显示一条提示已成功的警报.但是,如果活动指示器上没有计时器并且从未达到回调,那么它将永远旋转.将DispatchAsyc after计时器设置为15秒,如果未达到回调,则会显示错误标签.它总是有效的10次中的9次.

  1. 将数据发送到FB
  2. 显示活动指标
  3. 达到回调所以取消errorTask,将其设置为nil,并关闭活动指示器
  4. 显示成功警报.

但每一次都在

  1. 这需要15秒以上的时间
  2. firebaseTask 取消并设置为nil,活动指示器将被取消
  3. 错误标签会显示
  4. 成功警报仍然会出现

errorTask代码块驳回actiInd,示出了errorLabel,并取消firebaseTask,并将其设置到零.一旦firebaseTask被取消并设置为nil,我假设其中的所有内容也将停止,因为从未到达回调. 这可能是我混淆的原因.似乎即使firebaseTask取消并设置为nil,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")
    })

}
Run Code Online (Sandbox Code Playgroud)

Rob*_*Rob 9

这个取消程序没有做我怀疑你认为的那样.取消时DispatchWorkItem,它不执行抢先取消.它肯定与updateChildValues电话无关.它只是执行isCancelled属性的线程安全设置,如果您手动迭代循环,如果您发现任务被取消,您可以定期检查并提前退出.

因此,在isCancelled任务开始时检查并不是非常有用的模式,因为如果尚未创建任务,则没有任何内容可以取消.或者,如果任务已创建并添加到队列中,并在队列有机会启动之前取消,则显然只会取消但从未启动过,您将永远无法进行isCancelled测试.如果任务已经开始,那么isCancelledcancel调用之前它可能已经过了测试.

最重要的是,尝试对cancel请求进行计时,以便在任务开始之后但在它开始isCancelled测试之前准确接收它们将是徒劳的.你的比赛几乎不可能完美.此外,即使你确实碰巧完美地计时,这也只能说明整个过程是多么无效(只有百万分之一的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()
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以播放各种延迟,并查看延迟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()
}
Run Code Online (Sandbox Code Playgroud)

现在,你永远不会这样做,但它只是说明它isCancelled确实有效.

坦率地说,你永远不会这样使用isCancelled.isCancelled如果进行一些长时间的过程,您通常会使用该过程,您可以定期检查isCancelled状态,如果是真,则退出.但在你的情况下情况并非如此.

所有这一切的结论是,isCancelled在任务开始时检查不可能实现您所希望的.