GlobalScope 与 CoroutineScope 与生命周期范围

Dim*_*Dim 32 android coroutine kotlin-coroutines

AsyncTask由于它的简单性,我习惯于使用它并很好地理解它。但Coroutines让我感到困惑。您能否以简单的方式向我解释以下各项的区别和目的?

  1. GlobalScope.launch(Dispatchers.IO) {}
  2. GlobalScope.launch{}
  3. CoroutineScope(Dispatchers.IO).launch{}
  4. lifecycleScope.launch(Dispatchers.IO){}
  5. lifecycleScope.launch{}

Thr*_*ian 36

首先,让我们从定义开始说清楚。如果您需要 Coroutines 和 Coroutines Flow 的教程或游乐场,您可以查看我创建的这个教程/游乐场

Scope 是用于启动仅包含一个对象的协程的对象 CoroutineContext

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}
Run Code Online (Sandbox Code Playgroud)

协程上下文是一组规则和配置,用于定义协程将如何执行。在引擎盖下,它是一种映射,具有一组可能的键和值。

协程上下文是不可变的,但您可以使用加号运算符向上下文添加元素,就像向集合添加元素一样,生成新的上下文实例

定义协程行为的元素集是:

  • CoroutineDispatcher — 将工作分派到适当的线程。
  • Job——控制协程的生命周期。
  • CoroutineName — 协程的名称,用于调试。
  • CoroutineExceptionHandler — 处理未捕获的异常

调度程序 调度程序确定应该使用哪个线程池。Dispatchers 类也是 CoroutineContext,可以添加到 CoroutineContext

  • Dispatchers.Default:CPU 密集型工作,例如对大型列表进行排序、进行复杂计算等。JVM 上的共享线程池支持它。

  • Dispatchers.IO:网络或文件读写。简而言之——顾名思义,任何输入和输出

  • Dispatchers.Main:在 Android 的主线程或 UI 线程中执行 UI 相关事件的强制调度程序。

例如,在 RecyclerView 中显示列表、更新视图等。

您可以查看Android 的官方文档以获取有关调度程序的更多信息。

编辑即使官方文件指出

Dispatchers.IO - 此调度程序经过优化,可在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、读取或写入文件以及运行任何网络操作。

Marko Topolnic 的回答

IO 在一个特殊的、灵活的线程池上运行协程。当您被迫使用会阻塞其调用线程的旧式阻塞 IO API 时,它仅作为一种解决方法存在。

也可能是对的。

Job协程本身由 Job 表示。作业是协程的句柄。对于您创建的每个协程(通过启动或异步),它返回一个唯一标识协程并管理其生命周期的 Job 实例。您还可以将 Job 传递给 CoroutineScope 以控制其生命周期。

它负责协程的生命周期、取消和父子关系。可以从当前协程的上下文中检索当前作业:作业可以经历一组状态:新建、活动、完成、完成、取消和取消。虽然我们无法访问状态本身,但我们可以访问作业的属性:isActive、isCancelled 和 isCompleted。

CoroutineScope定义了一个简单的工厂函数,它以CoroutineContexts 作为参数来围绕组合的 CoroutineContext 创建包装器作为

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    // CoroutineScope is used intentionally for user-friendly representation
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}
Run Code Online (Sandbox Code Playgroud)

Job如果提供上下文还没有一个元素,则创建一个元素。

我们看一下 GlobalScope 源代码

/**
 * A global [CoroutineScope] not bound to any job.
 *
 * Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
 * and are not cancelled prematurely.
 * Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them.
 *
 * Application code usually should use an application-defined [CoroutineScope]. Using
 * [async][CoroutineScope.async] or [launch][CoroutineScope.launch]
 * on the instance of [GlobalScope] is highly discouraged.
 *
 * Usage of this interface may look like this:
 *
 * ```
 * fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> = GlobalScope.produce(Dispatchers.Unconfined) {
 *     for (number in this) {
 *         send(Math.sqrt(number))
 *     }
 * }
 * ```
 */
public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,它延伸 CoroutineScope

1- 只要您的应用程序还活着,GlobalScope 就一直存在,例如,如果您在此范围内进行一些计数并旋转您的设备,它将继续执行任务/进程。

GlobalScope.launch(Dispatchers.IO) {} 
Run Code Online (Sandbox Code Playgroud)

只要您的应用程序处于活动状态,但由于使用而在 IO 线程中运行 Dispatchers.IO

2-它与第一个相同,但默认情况下,如果您没有任何上下文,则启动使用使用 Dispatchers.Default 的 EmptyCoroutineContext,所以唯一的区别是线程与第一个。

3- 这个和第一个一样,只是语法不同。

4-lifecycleScopeLifeCycleOwner对 Activity 或 Fragment 的生命周期的扩展并绑定到该生命周期,其中当该 Activity 或 Fragment 被销毁时范围被取消。

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
Run Code Online (Sandbox Code Playgroud)

您也可以将其用作

class Activity3CoroutineLifecycle : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main + CoroutineName(" Activity Scope") + CoroutineExceptionHandler { coroutineContext, throwable ->
            println(" Exception $throwable in context:$coroutineContext")
        }


    private val dataBinding by lazy {
        Activity3CoroutineLifecycleBinding.inflate(layoutInflater)
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(dataBinding.root)
    
        job = Job()

        dataBinding. button.setOnClickListener {

            // This scope lives as long as Application is alive
            GlobalScope.launch {
                for (i in 0..300) {
                    println(" Global Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
                    delay(300)
                }
            }

            // This scope is canceled whenever this Activity's onDestroy method is called
            launch {
                for (i in 0..300) {
                    println(" Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
                    withContext(Dispatchers.Main) {
                        dataBinding.tvResult.text = " Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this"
                    }
                    delay(300)
                }
            }
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }

}
Run Code Online (Sandbox Code Playgroud)

  • “Dispatchers.Main:推荐的用于执行 UI 相关事件的调度程序。”——不仅仅是推荐,而是_强制_。不使用它来使用 GUI 会导致应用程序崩溃。 (2认同)

Sid*_*ria 12

TL; 博士

  1. GlobalScope.launch(Dispatchers.IO):在 上启动顶级协程Dispatchers.IO。协程未绑定并一直运行直到完成或取消。通常不鼓励,因为程序员必须维护对join()or的引用cancel()

  2. GlobalScope.launch:同上,但GlobalScope用途Dispatchers.Default如果没有指定。经常心灰意冷。

  3. CoroutineScope(Dispatchers.IO).launch:创建一个协程范围,Dispatchers.IO除非在协程构建器中指定了调度程序,否则它会使用launch

  4. CoroutineScope(Dispatchers.IO).launch(Dispatchers.Main):奖励之一。使用与上述相同的协程作用域(如果作用域实例相同!),但为此协程覆盖Dispatcher.IOwith Dispatchers.Main

  5. LifecycleScope.launch (Dispatchers.IO):在 AndroidX 提供的生命周期范围内启动一个协程。一旦生命周期失效(即用户导航离开片段),协程就会被取消。用途Dispatchers.IO为线程池。

  6. LifecycleScope.launch:与上面相同,但Dispatchers.Main如果未指定则使用。

解释

协程范围促进了结构化并发,您可以在同一范围内启动多个协程并在需要时取消该范围(进而取消该范围内的所有协程)。相反,GlobalScope 协程类似于线程,您需要在其中保持对join()or的引用cancel()。这是Roman Elizarov 在 Medium 上发表的一篇优秀文章。

CoroutineDispatcher告诉协程构建器(在我们的例子中launch {})要使用哪个线程池。有一些预定义的 Dispatchers 可用。

  • Dispatchers.Default- 使用相当于 CPU 内核数的线程池。应该用于 CPU 密集型工作负载。
  • Dispatchers.IO- 使用 64 个线程池。非常适合 IO 绑定工作负载,其中线程通常在等待;也许用于网络请求或磁盘读/写。
  • Dispatchers.Main(仅限 Android):使用主线程执行协程。非常适合更新 UI 元素。

例子

我写了一个小demo片段,里面有6个函数对应上面6个场景。如果您在 Android 设备上运行以下片段;打开片段,然后离开片段;你会注意到只有 GlobalScope 协程还活着。当生命周期无效时,生命周期协程被生命周期范围取消。另一方面,CoroutineScope 在onPause()我们明确完成的调用时被取消。

class DemoFragment : Fragment() {

    private val coroutineScope = CoroutineScope(Dispatchers.IO)

    init {
        printGlobalScopeWithIO()
        printGlobalScope()
        printCoroutineScope()
        printCoroutineScopeWithMain()
        printLifecycleScope()
        printLifecycleScopeWithIO()
    }

    override fun onPause() {
        super.onPause()
        coroutineScope.cancel()
    }

    private fun printGlobalScopeWithIO() = GlobalScope.launch(Dispatchers.IO) {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[GlobalScope-IO] I'm alive on thread ${Thread.currentThread().name}!")
        }
    }

    private fun printGlobalScope() = GlobalScope.launch {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[GlobalScope] I'm alive on ${Thread.currentThread().name}!")
        }
    }
    
    private fun printCoroutineScope() = coroutineScope.launch {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[CoroutineScope] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[CoroutineScope] I'm exiting!")
    }

    private fun printCoroutineScopeWithMain() = coroutineScope.launch(Dispatchers.Main) {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm exiting!")
    }

    private fun printLifecycleScopeWithIO() = lifecycleScope.launch(Dispatchers.IO) {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[LifecycleScope-IO] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[LifecycleScope-IO]  I'm exiting!")
    }

    private fun printLifecycleScope() = lifecycleScope.launch {
        while (isActive) {
            delay(1000)
            Log.d("CoroutineDemo", "[LifecycleScope] I'm alive on ${Thread.currentThread().name}!")
        }
        Log.d("CoroutineDemo", "[LifecycleScope] I'm exiting!")
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 如果不取消`CoroutineScope`会发生什么?它的行为像“GlobalScope”吗? (2认同)

i30*_*mb1 9

您应该知道,如果您想启动suspend功能,您需要在CoroutineScope. 每个CoroutineScope都有CoroutineContext。其中可以包含(将工作分派到适当的线程)、 (控制协程的生命周期)、(处理未捕获的异常)、(协程的名称,对于调试有用)的CoroutineContext映射。DispatcherJobCoroutineExceptionHandlerCoroutineName

  1. GlobalScope.launch(Dispatchers.IO) {}-GlobalScope.launch创建全局协程并用于不应取消的操作,但更好的替代方案是在 Application 类中创建自定义范围,并将其注入到需要它的类中。这样做的优点是让您能够使用CoroutineExceptionHandler或替换CoroutineDispatcher进行测试。
  2. GlobalScope.launch{}- 与 相同,GlobalScope.launch(Dispatchers.IO) {}但运行coroutinesDispatchers.Default. 如果在上下文中没有指定调度程序,则使用Dispatchers.Default默认值。Dispatcher
  3. CoroutineScope(Dispatchers.IO).launch{}- 它使用一个参数创建范围并coroutineIO线程上启动新的范围。将被发射的物体摧毁。但如果你想正常结束你的工作,你应该手动.cancel() 调用。CoroutineScope
  4. lifecycleScope.launch(Dispatchers.IO){}Lifecycle- 它是可从 a或LifecycleOwner(Activity或)获得的现有范围Fragment,并以依赖关系出现在您的项目中androidx.lifecycle:lifecycle-runtime-ktx:*。使用它您可以摆脱手动创建CoroutineScope。它将Dispatchers.IO无阻塞地运行你的作业MainThread,并确保当你的作业lifecycle被销毁时,你的作业将被取消。
  5. lifecycleScope.launch{}-与使用默认参数为您lifecycleScope.launch(Dispatchers.IO){}创建并运行您的相同,这意味着您可以使用.CoroutinesScopeDispatchers.MaincoroutinesDispatcher.MainUI


Mar*_*nik 9

我会沿着三个轴组织你的列表:

  1. GlobalScope对比CoroutineScope()对比lifecycleScope
  2. Dispatchers.IO 与继承(隐式)调度程序
  3. 指定范围内的调度程序 vs. 作为参数 launch

1. 范围的选择

Kotlin 对协程的很大一部分是结构化并发,这意味着所有协程都被组织成一个层次结构,遵循它们的依赖关系。如果您正在启动一些后台工作,我们假设您希望它的结果在当前“工作单元”仍然处于活动状态的某个时刻出现,即,用户还没有离开它并且不再关心它的结果。

在 Android 上,您可以使用lifecycleScope自动跟随用户在 UI 活动中的导航,因此您应该将其用作后台工作的父级,其结果将对用户可见。

您可能还有一些“即发即忘”的工作,您只需要最终完成这些工作,但用户不会等待其结果。为此,您应该使用 AndroidWorkManager或类似的功能,即使用户切换到另一个应用程序也可以安全地继续。这些通常是将本地状态与服务器端保存的状态同步的任务。

在这张图片中,GlobalScope基本上是结构化并发的逃生舱口。它允许您满足提供范围的形式,但破坏了它应该实现的所有机制。GlobalScope永远无法取消并且它没有父级。

编写CoroutineScope(...).launch是错误的,因为您创建了一个没有父级的作用域对象,您会立即忘记,因此无法取消它。它类似于使用,GlobalScope但更加hacky。

2. 调度员的选择

协程调度器决定您的协程可以运行在哪些线程上。在 Android 上,您应该关注三个调度程序:

  1. Main在单个 GUI 线程上运行所有内容。它应该是您的主要选择。
  2. IO在一个特殊的、灵活的线程池上运行协程。当您被迫使用会阻塞其调用线程的旧式阻塞 IO API 时,它仅作为一种解决方法存在。
  3. Default还使用线程池,但大小固定,等于 CPU 内核数。将它用于计算密集型工作,这些工作需要足够长的时间导致 GUI 出现故障(例如,图像压缩/解压缩)。

3. 在哪里指定调度员

首先,您应该了解您正在使用的协程范围中指定的调度程序。GlobalScope没有指定任何,所以一般默认有效,Default调度员。lifecycleScope指定Main调度程序。

我们已经解释过您不应该使用CoroutineScope构造函数创建临时作用域,因此指定显式调度程序的正确位置是作为 的参数launch

在技​​术细节上,当您编写 时someScope.launch(someDispatcher)someDispatcher参数实际上是一个成熟的协程上下文对象,它恰好有一个元素,即调度程序。您正在启动的协程通过将协程范围内的协程与您作为参数提供的协程组合起来,为自己创建了一个新的上下文。最重要的是,它Job为自己创造了一个新鲜事物并将其添加到上下文中。该作业是上下文中继承的作业的子代。