阅读 346
Kotlin|第一个协程

Kotlin|第一个协程

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

第一个协程程序

本质上,协程是轻量级的线程。 它们在某些 [CoroutineScope] 协程构建器 一起启动。 这里我们在 [GlobalScope]中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制

var globalScopeFun = GlobalScope.launch( context = Dispatchers.IO) {
    delay(3000)
    Log.i("coroutines","GlobalScope: hello world")
}

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.i("coroutines","GlobalScope:start")

        globalScopeFun//协程代码

        Log.i("coroutines","GlobalScope:end")

    }
}
复制代码
GlobalScope:start
GlobalScope:end
GlobalScope:hello world
复制代码

在上面的例子中,通过GlobalScope启动一个协程,并且延时了三秒输入日志,从结果上看 可以将 GlobalScope.launch { …… } 替换为 thread { …… }, 并将 delay(……) 替换为 Thread.sleep(……) 达到同样目的。但是协程和线程还是有区别的,协程可以更好的管理,可以极大的提高并发效率,减少代码回调提高可读性.

  1. 通过以上代码我们引出4个概念
  1. suspend fun : delay 是一个挂起函数 "public suspend fun delay(timeMillis: Long)",在给定时间内延迟协程而不阻塞线程,并在指定时间后恢复协程。
  2. CoroutineScope :定义新协程的作用域。GlobalScope是CoroutineScope的实现类,用来管理协程的生命周期,协程的启动需要通过CoroutineScope
  3. CoroutineContext :Dispatchers.IO就是抽象的CoroutineContext,定义Dispatchers使用的最大线程数的属性名。IO)协同程序调度程序
  4. CoroutineBuilder :协程构造器,CoroutineContext是用过协程构造器launch,async声明启动的,launch,async是CoroutineScope的扩展方法

通过上面的4条概念,我们逐条分析它们的原理和使用场景

suspend fun 挂起函数

  1. 错误信息

如果你首先将 GlobalScope.launch 替换为 thread,编译器会报以下错误: Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

这是因为 delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会挂起协程,并且只能在协程中使用。

public suspend fun delay(timeMillis: Long)
复制代码

delay 用了suspend修饰,用suspend修饰的就是挂起函数,我们在看 delay源码的时候有这样一句话" Delays coroutine for a given time without blocking a thread and resumes it after a specified time."翻译过来就是:在给定时间内延迟协程而不阻塞线程,并在指定时间后恢复协程, delay函数类似java中的Thread.sleep(),之所以说delay是不阻塞线程,是因为thread的生命周期由系统控制的,而线程的delay是可以交给开发者来控制的,在源码中有这样一句话"This suspending function is cancellable."意思就是这个暂停功能是可以取消的如果当前协程的Job在这个挂起函数等待时被取消或完成,则此函数立即恢复所以delay是不存在线程阻塞的.

取消协程的执行

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancel() // 取消该作业
    job.join() // 等待作业执行结束
    println("main: Now I can quit.")
}
复制代码

程序执行后的输出如下:

job: I'm sleeping 0 ... 
job: I'm sleeping 1 ... 
job: I'm sleeping 2 ... 
main: I'm tired of waiting! 
main: Now I can quit.
复制代码

一旦 main 函数调用了 job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了.

协程作用域

让我们将关于上下文,子协程以及作业的知识综合在一起。假设我们的应用程序拥有一个具有生命周期的对象,但这个对象并不是一个协程。举例来说,我们编写了一个 Android 应用程序并在 Android 的 activity 上下文中启动了一组协程来使用异步操作拉取并更新数据以及执行动画等等。所有这些协程必须在这个 activity 销毁的时候取消以避免内存泄漏。当然,我们也可以手动操作上下文与作业,以结合 activity 的生命周期与它的协程,但是 kotlinx.coroutines 提供了一个封装:[CoroutineScope]的抽象。 你应该已经熟悉了协程作用域,因为所有的协程构建器都声明为在它之上的扩展。

我们通过创建一个 [CoroutineScope]实例来管理协程的生命周期,并使它与 activity 的生命周期相关联。CoroutineScope 可以通过 [CoroutineScope()]创建或者通过[MainScope()]工厂函数。前者创建了一个通用作用域,而后者为使用 [Dispatchers.Main] 作为默认调度器的 UI 应用程序 创建作用域

class Activity {
    private val mainScope = MainScope()

    fun destroy() {
        mainScope.cancel()
    }
    // 继续运行……
复制代码

现在,我们可以使用定义的 scope 在这个 Activity 的作用域内启动协程。 对于该示例,我们启动了十个协程,它们会延迟不同的时间:

// 在 Activity 类中
    fun doSomething() {
        // 在示例中启动了 10 个协程,且每个都工作了不同的时长
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间
                println("Coroutine $i is done")
            }
        }
    }
} // Activity 类结束
复制代码

在 main 函数中我们创建 activity,调用测试函数 doSomething,并且在 500 毫秒后销毁这个 activity。 这取消了从 doSomething 启动的所有协程。我们可以观察到这些是由于在销毁之后, 即使我们再等一会儿,activity 也不再打印消息。

import kotlinx.coroutines.*

class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
    
    fun destroy() {
        mainScope.cancel()
    }

    fun doSomething() {
        // 在示例中启动了 10 个协程,且每个都工作了不同的时长
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间
                println("Coroutine $i is done")
            }
        }
    }
} // Activity 类结束

fun main() = runBlocking<Unit> {
    val activity = Activity()
    activity.doSomething() // 运行测试函数
    println("Launched coroutines")
    delay(500L) // 延迟半秒钟
    println("Destroying activity!")
    activity.destroy() // 取消所有的协程
    delay(1000) // 为了在视觉上确认它们没有工作    
}
复制代码

这个示例的输出如下所示:

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!
复制代码

你可以看到,只有前两个协程打印了消息,而另一个协程在 Activity.destroy() 中单次调用了 job.cancel()

使用 async

  1. 使用 async 并发

在概念上,async 就类似于 launch它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch 返回一个 Job 并且不附带任何结果值,而 async 返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await() 在一个延期的值上得到它的最终结果, 但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 假设我们在这里做了些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 假设我们在这里也做了些有用的事
    return 29
}
复制代码

它的打印输出如下:

The answer is 42
Completed in 1017 ms
复制代码

这里快了两倍,因为两个协程并发执行。 请注意,使用协程进行并发总是显式的。

  1. 惰性启动的 async

可选的,[async] 可以通过将 start 参数设置为 [CoroutineStart.LAZY]而变为惰性的。 在这个模式下,只有结果通过 [await] 获取的时候协程才会启动,或者在 Job 的 [start] 函数调用的时候。运行下面的示例

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // 执行一些计算
        one.start() // 启动第一个
        two.start() // 启动第二个
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 假设我们在这里做了些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 假设我们在这里也做了些有用的事
    return 29
}
复制代码

它的打印输出如下:

The answer is 42
Completed in 1017 ms
复制代码

因此,在先前的例子中这里定义的两个协程没有执行,但是控制权在于程序员准确的在开始执行时调用 [start]我们首先 调用 one,然后调用 two,接下来等待这个协程执行完毕。

注意,如果我们只是在 println 中调用 [await]而没有在单独的协程中调用 [start],这将会导致顺序行为,直到 [await]启动该协程 执行并等待至它结束,这并不是惰性的预期用例。 在计算一个值涉及挂起函数时,这个 async(start = CoroutineStart.LAZY) 的用例用于替代标准库中的 lazy 函数。

在 finally 中释放资源

我们通常使用如下的方法处理在被取消时抛出 [CancellationException] 的可被取消的挂起函数。比如说,try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执行它们的终结动作:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("job: I'm running finally")
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并且等待它结束
    println("main: Now I can quit.")
}
复制代码

join和 cancelAndJoin等待了所有的终结动作执行完毕, 所以运行示例得到了下面的输出:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.
复制代码
文章分类
Android