简体   繁体   English

使部分协程继续过去取消

[英]Make part of coroutine continue past cancellation

I have a file managing class that can save a big file.我有一个可以保存大文件的文件管理类。 The file manager class is an application singleton, so it outlives my UI classes.文件管理器类是一个应用程序单例,因此它的寿命比我的 UI 类长。 My Activity/Fragment can call the save suspend function of the file manager from a coroutine and then show success or failure in the UI.我的 Activity/Fragment 可以从协程调用文件管理器的save挂起功能,然后在 UI 中显示成功或失败。 For example:例如:

//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
    try {
        myFileManager.saveBigFile()
        myTextView.text = "Successfully saved file"
    } catch (e: IOException) {
        myTextView.text = "Failed to save file"
    }
}

//In MyFileManager
suspend fun saveBigFile() {
    //Set up the parameters
    //...

    withContext(Dispatchers.IO) {
        //Save the file
        //...
    }
}

The problem with this approach is that I don't want the save operation to be aborted if the Activity is finished.这种方法的问题在于,如果 Activity 完成,我不希望保存操作被中止。 If the activity is destroyed before the withContext block gets going, or if the withContext block has any suspension points in it, then saving will not be completed because the coroutine will be canceled.如果活动在withContext块开始之前被销毁,或者如果withContext块中有任何暂停点,那么保存将不会完成,因为协程将被取消。

What I want to happen is that the file is always saved.我想要发生的是文件总是被保存。 If the Activity is still around, then we can show UI updates on completion.如果 Activity 仍然存在,那么我们可以在完成时显示 UI 更新。

I thought one way to do it might be to start a new coroutineScope from the suspend function like this, but this scope still seems to get cancelled when its parent job is cancelled.我认为一种方法可能是像这样从挂起函数启动一个新的coroutineScope ,但是当它的父作业被取消时,这个范围似乎仍然被取消。

suspend fun saveBigFile() = coroutineScope {
    //...
}

I thought another alternative might be to make this a regular function that updates some LiveData when it's finished.我认为另一种选择可能是使其成为一个常规函数,在它完成时更新一些 LiveData。 The Activity could observe the live data for the result, and since LiveData automatically removes lifecycle observers when they're destroyed, the Activity is not leaked to the FileManager. Activity 可以观察结果的实时数据,并且由于 LiveData 在生命周期观察者被销毁时会自动删除它们,因此 Activity 不会泄漏到 FileManager。 I'd like to avoid this pattern if the something less convoluted like the above can be done instead.如果可以做一些像上面那样不那么复杂的事情,我想避免这种模式。

//In MyActivity:
private fun saveTheFile() {
    val result = myFileManager.saveBigFile()
    result.observe(this@MyActivity) {
        myTextView.text = when (it) {
            true -> "Successfully saved file"
            else -> "Failed to save file"
        }
    }
}

//In MyFileManager
fun saveBigFile(): LiveData<Boolean> {
    //Set up the parameters
    //...
    val liveData = MutableLiveData<Boolean>()
    MainScope().launch {
        val success = withContext(Dispatchers.IO) {
            //Save the file
            //...
        }
        liveData.value = success
    }
    return liveData
}

You can wrap the bit that you don't want to be cancelled with NonCancellable .您可以使用NonCancellable包装不想取消的NonCancellable

// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
    // Will complete, even if cancelled.
}
// May cancel here.

If you have code whose lifetime is scoped to the lifetime of the whole application, then this is a use case for the GlobalScope .如果您的代码的生命周期范围为整个应用程序的生命周期,那么这是GlobalScope一个用例。 However, just saying GlobalScope.launch is not a good strategy because you could launch several concurrent file operations that may be in conflict (this depends on your app's details).但是,仅仅说GlobalScope.launch并不是一个好的策略,因为您可能会启动多个可能发生冲突的并发文件操作(这取决于您的应用程序的详细信息)。 The recommended way is to use a globally-scoped actor , in the role of an executor service.推荐的方法是使用全局作用域的actor ,充当执行器服务的角色。

Basically, you can say基本上,你可以说

@ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
    for (task in channel) {
        task()
    }
}

And use it like this:并像这样使用它:

private fun saveTheFile() = lifecycleScope.launch {
    executor.send {
        try {
            myFileManager.saveBigFile()
            withContext(Main) {
                myTextView.text = "Successfully saved file"
            }
        } catch (e: IOException) {
            withContext(Main) {
                myTextView.text = "Failed to save file"
            }
        }
    }
}

Note that this is still not a great solution, it retains myTextView beyond its lifetime.请注意,这仍然不是一个很好的解决方案,它myTextView在其生命周期之后保留myTextView Decoupling the UI notifications from the view is another topic, though.不过,将 UI 通知与视图分离是另一个主题。

actor is labeled as "obsolete coroutines API", but that's just an advance notice that it will be replaced with a more powerful alternative in a future version of Kotlin. actor被标记为“过时的协程 API”,但这只是一个预先通知,它将在 Kotlin 的未来版本中被更强大的替代品所取代。 It doesn't mean it's broken or unsupported.这并不意味着它已损坏或不受支持。

I tried this, and it appears to do what I described that I wanted.我试过这个,它似乎做了我所描述的我想要的。 The FileManager class has its own scope, though I suppose it could also be GlobalScope since it's a singleton class. FileManager 类有自己的作用域,不过我想它也可以是 GlobalScope,因为它是一个单例类。

We launch a new job in its own scope from the coroutine.我们从协程在其自己的范围内启动一个新作业。 This is done from a separate function to remove any ambiguity about the scope of the job.这是从一个单独的函数完成的,以消除有关工作范围的任何歧义。 I use async for this other job so I can bubble up exceptions that the UI should respond to.我将async用于其他工作,以便我可以冒出 UI 应该响应的异常。

Then after launch, we await the async job back in the original scope.然后在启动后,我们等待异步作业回到原始范围。 await() suspends until the job is completed and passes along any throws (in my case I want IOExceptions to bubble up for the UI to show an error message). await()挂起直到作业完成并传递任何抛出(在我的情况下,我希望 IOExceptions 冒泡以便 UI 显示错误消息)。 So if the original scope is cancelled, its coroutine never waits for the result, but the launched job keeps rolling along until it completes normally.所以如果原来的作用域被取消了,它的协程从不等待结果,但是启动的作业会一直滚动直到它正常完成。 Any exceptions that we want to ensure are always handled should be handled within the async function.我们要确保始终处理的任何异常都应在异步函数中处理。 Otherwise, they won't bubble up if the original job is cancelled.否则,如果取消原​​始作业,它们将不会冒泡。

//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
    try {
        myFileManager.saveBigFile()
        myTextView.text = "Successfully saved file"
    } catch (e: IOException) {
        myTextView.text = "Failed to save file"
    }
}

class MyFileManager private constructor(app: Application):
    CoroutineScope by MainScope() {

    suspend fun saveBigFile() {
        //Set up the parameters
        //...

        val deferred = saveBigFileAsync()
        deferred.await()
    }

    private fun saveBigFileAsync() = async(Dispatchers.IO) {
        //Save the file
        //...
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM