Coroutine launch() 而在另一个协程中

Coroutine launch() while inside another coroutine

我有以下代码(伪代码)

fun onMapReady()
{
    //do some stuff on current thread (main thread)

    //get data from server
    GlobalScope.launch(Dispatchers.IO){

        getDataFromServer { result->

            //update UI on main thread
            launch(Dispatchers.Main){
                updateUI(result) //BREAKPOINT HERE NEVER CALLED
            }
        }

    }
}

如评论所述,代码从未进入协程调度到主队列。但是,如果我明确使用 GlobalScope.launch(Dispatchers.Main) 而不仅仅是 launch(Dispatchers.Main)

,则下面的内容有效
fun onMapReady()
{
    //do some stuff on current thread (main thread)

    //get data from server
    GlobalScope.launch(Dispatchers.IO){

        getDataFromServer { result->

            //update UI on main thread
            GlobalScope.launch(Dispatchers.Main){
                updateUI(result) //BREAKPOINT HERE IS CALLED
            }
        }

    }
}

为什么第一种方法不起作用?

我认为这里的问题是 getDataFromServer() 是异步的,它立即 returns 因此您在退出 GlobalScope.launch(Dispatchers.IO) { ... } 块后调用 launch(Dispatchers.Main) 。换句话说:您尝试使用已经完成的协程范围启动协程。

我的建议是不要像这样将异步的、基于回调的 API 与协同程序混合使用。协程最适合与同步的挂起函数一起使用。此外,如果您更喜欢异步执行所有内容并独立于其他任务(您的 onMapReady() 启动了 3 个独立的异步操作),那么我认为协程根本不是一个好的选择。

关于您的示例:您确定不能直接从主线程执行 getDataFromServer() 吗?它不应该阻塞主线程,因为它是异步的。同样,在某些库中,回调会在主线程中自动执行,在这种情况下,您的示例可以替换为:

fun onMapReady() {
    getDataFromServer { result->
        updateUI(result)
    }
}

如果结果在后台线程中执行,那么您可以像以前一样使用 GlobalScope.launch(Dispatchers.Main),但这并不是我们使用协程的通常方式。或者您可以使用实用程序,例如runOnUiThread() 在 Android 上,这可能更有意义。

@broot 已经解释了问题的要点。您正在尝试 launch 外部 GlobalScope.launch 的子作用域中的协程,但是当调用 getDataFromServer 的回调时该作用域已经完成。

所以简而言之,不要在将在您无法控制的 place/time 中调用的回调中捕获外部范围。

处理您的问题的更好方法是使 getDataFromServer 挂起而不是基于回调。如果它是您无法控制的 API,您可以通过这种方式创建一个暂停包装器:

suspend fun getDataFromServerSuspend(): ResultType = suspendCoroutine { cont ->
    getDataFromServer { result ->
        cont.resume(result)
    }
}

然后您可以简化调用代码:

fun onMapReady() {

    // instead of GlobalScope, please use viewModelScope or lifecycleScope,
    // or something more relevant (see explanation below)
    GlobalScope.launch(Dispatchers.IO) {

        val result = getDataFromServer()

        // you don't need a separate coroutine, just a context switch
        withContext(Dispatchers.Main) {
            updateUI(result)
        }
    }
}

附带说明一下,GlobalScope 可能不是您想要的。您应该改为使用映射到视图或视图模型生命周期的范围(viewModelScopelifecycleScope),因为如果视图被销毁,您对该协程的结果不感兴趣(因此它应该取消)。如果由于某种原因某些东西在协程内挂起或循环,这将避免协程泄漏。