正确取消 Kotlin 协程任务

3

我很困惑如何正确取消协程任务。 测试案例很简单,我有一个类,其中包含两个方法:

class CancellationTest {
   private var job: Job? = null
   private var scope = MainScope()

   fun run() {
      job?.cancel()
      job = scope.launch { doWork() }
   }

   fun doWork() {
      // gets data from some source and send it to BE
   }
}

方法doWork有一个api调用,它是暂停的并且尊重取消

在上面的例子中,当计算成功发送到后端的对象后,我可以看到很多重复项,这意味着cancel并没有真正地取消之前的调用。

然而,如果我使用在互联网上找到的片段

internal class WorkingCancellation<T> {
    private val activeTask = AtomicReference<Deferred<T>?>(null)
    suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
        activeTask.get()?.cancelAndJoin()

        return coroutineScope {
            val newTask = async(start = CoroutineStart.LAZY) {
                block()
            }

            newTask.invokeOnCompletion {
                activeTask.compareAndSet(newTask, null)
            }

            val result: T

            while (true) {
                if (!activeTask.compareAndSet(null, newTask)) {
                    activeTask.get()?.cancelAndJoin()
                    yield()
                } else {
                    result = newTask.await()
                    break
                }
            }

            result
        }
    }
}

它正常工作,对象不会重复并且已正确发送到BE。 最后一件事是我在for循环中调用run方法-但无论如何,我不确定为什么job?.cancel不能正常工作,而WorkingCancellation实际上正在工作。

2个回答

4

除了Sam的回答外,考虑这个例子,它模拟了一个连续的交易,比如位置更新到服务器。

var pingInterval = System.currentTimeMillis()
job = launch {
     while (true) {
         if (System.currentTimeMillis() > pingInterval) {
             Log.e("LocationJob", "Executing location updates... ")
             pingInterval += 1000L
         }
     }
}

持续不断地将位置信息通过UDP协议发送到服务器,或者像其他常见用例一样,持续从服务器中获取某些数据。

然后我有一个被按钮调用的函数,用于取消这个job操作。

fun cancel() {
    job.cancel()
    Log.e("LocationJob", "Location updates done.")
}

当调用此函数时,job会被取消,但是操作仍在继续进行,因为没有确保协程范围停止工作,所有上述操作都将打印。
 Ping server my location...
 Ping server my location...
 Ping server my location...
 Ping server my location...
 Location updates done.
 Ping server my location...
 Ping server my location...

现在,如果我们将ensureActive()插入无限循环中。
while (true) {
     ensureActive()
     if (System.currentTimeMillis() > pingInterval) {
          Log.e("LocationJob", "Ping server my location... ")
           pingInterval += 1000L
     }
}

取消job将保证操作停止。我尝试使用delay,当调用job的任务取消时,它保证完全的cancellation。加入 ensureActive() ,并在2秒后取消,输出
 Ping server my location...
 Ping server my location...
 Location updates done.

3
简短回答:只有在调用挂起库函数时,取消才能立即生效。非挂起代码需要手动检查以使其可取消。
Kotlin协程中的取消是协作性的,需要被取消的任务检查是否已取消并终止其正在执行的工作。如果任务不检查取消,它可以愉快地继续运行而永远不会发现自己已被取消。
当您调用内置的挂起函数时,协程会自动检查取消状态。如果您查看常用挂起函数(例如await()yield())的文档,您将看到它们总是说“此挂起函数可取消”。
您的doWork不是一个suspend函数,因此它不能调用任何其他挂起函数,因此永远不会触发这些自动取消检查之一。如果你确实想要取消它,你需要周期性地检查工作是否仍处于活动状态,或者改变其实现以使用挂起函数。您可以通过在Job上调用ensureActive手动检查取消。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接