簡體   English   中英

使部分協程繼續過去取消

[英]Make part of coroutine continue past cancellation

我有一個可以保存大文件的文件管理類。 文件管理器類是一個應用程序單例,因此它的壽命比我的 UI 類長。 我的 Activity/Fragment 可以從協程調用文件管理器的save掛起功能,然后在 UI 中顯示成功或失敗。 例如:

//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
        //...
    }
}

這種方法的問題在於,如果 Activity 完成,我不希望保存操作被中止。 如果活動在withContext塊開始之前被銷毀,或者如果withContext塊中有任何暫停點,那么保存將不會完成,因為協程將被取消。

我想要發生的是文件總是被保存。 如果 Activity 仍然存在,那么我們可以在完成時顯示 UI 更新。

我認為一種方法可能是像這樣從掛起函數啟動一個新的coroutineScope ,但是當它的父作業被取消時,這個范圍似乎仍然被取消。

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

我認為另一種選擇可能是使其成為一個常規函數,在它完成時更新一些 LiveData。 Activity 可以觀察結果的實時數據,並且由於 LiveData 在生命周期觀察者被銷毀時會自動刪除它們,因此 Activity 不會泄漏到 FileManager。 如果可以做一些像上面那樣不那么復雜的事情,我想避免這種模式。

//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
}

您可以使用NonCancellable包裝不想取消的NonCancellable

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

如果您的代碼的生命周期范圍為整個應用程序的生命周期,那么這是GlobalScope一個用例。 但是,僅僅說GlobalScope.launch並不是一個好的策略,因為您可能會啟動多個可能發生沖突的並發文件操作(這取決於您的應用程序的詳細信息)。 推薦的方法是使用全局作用域的actor ,充當執行器服務的角色。

基本上,你可以說

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

並像這樣使用它:

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"
            }
        }
    }
}

請注意,這仍然不是一個很好的解決方案,它myTextView在其生命周期之后保留myTextView 不過,將 UI 通知與視圖分離是另一個主題。

actor被標記為“過時的協程 API”,但這只是一個預先通知,它將在 Kotlin 的未來版本中被更強大的替代品所取代。 這並不意味着它已損壞或不受支持。

我試過這個,它似乎做了我所描述的我想要的。 FileManager 類有自己的作用域,不過我想它也可以是 GlobalScope,因為它是一個單例類。

我們從協程在其自己的范圍內啟動一個新作業。 這是從一個單獨的函數完成的,以消除有關工作范圍的任何歧義。 我將async用於其他工作,以便我可以冒出 UI 應該響應的異常。

然后在啟動后,我們等待異步作業回到原始范圍。 await()掛起直到作業完成並傳遞任何拋出(在我的情況下,我希望 IOExceptions 冒泡以便 UI 顯示錯誤消息)。 所以如果原來的作用域被取消了,它的協程從不等待結果,但是啟動的作業會一直滾動直到它正常完成。 我們要確保始終處理的任何異常都應在異步函數中處理。 否則,如果取消原​​始作業,它們將不會冒泡。

//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