7

我使用 GCDDispatchWorkItem来跟踪发送到 firebase 的数据。

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

第一个属性名为errorTask。初始化它cancels并将firebaseTask其设置为nil然后打印“errorTaskfired”。如果DispatchAsync Timer在此之前没有取消 errorTask,它将在 0.0000000001 秒内调用它。

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

问题是内部的代码errorTask总是在firebaseTask到达回调之前运行。errorTask代码取消并将其firebaseTask设置为 nil 但由于某种原因firebaseTask仍然运行。我想不通为什么?

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

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

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

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

但每隔一段时间

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

代码块关闭actiInd errorTask,显示 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")
    })

}
4

1 回答 1

15

这个取消例程并没有像我怀疑的那样做。当您取消 aDispatchWorkItem时,它不会执行抢先取消。updateChildValues它当然与通话无关。它所做的只是执行isCancelled属性的线程安全设置,如果您手动迭代循环,您可以定期检查并在看到任务被取消时提前退出。

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

最重要的是,尝试对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()
}

现在您可以玩各种延迟,并查看 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在任务开始时进行检查不太可能实现您所希望的。

于 2018-02-17T18:32:16.003 回答