文章目录

  • 一、前言
  • 二、线程与协程的区别
  • 线程需要大量的资源。
  • 不可预测的行为
  • 竞态条件
  • 三、协程
  • GlobalScope.launch
  • runBlocking 简述
  • async()
  • 何时将函数标记为 suspend
  • 四、线程的切换
  • 五、参考链接


一、前言

在kotlin中协程用法是比较广泛的,协程也是比较复杂的,本文只对其做个简单的记录,后面再进行详细描述

二、线程与协程的区别

由于有些耗时操作需要等待,因此常常放在子线程中去操作,用来提升用户体验。但是线程存在一些以下问题(这些问题其实可以解决,只是比较麻烦),所以kotlin建议使用协程来处理异步任务

线程需要大量的资源。

但是因为线程的创建、切换和管理线程会占用系统资源和时间,从而限制能够同时管理的原始线程数量。创建成本也会大幅增加。

不可预测的行为

线程是一种抽象表示,用于说明处理器如何同时处理多个任务。由于处理器会在不同线程上的各个指令集之间切换,因此您无法控制线程的确切执行时间和暂停时间。直接使用线程时,您不能总是期望能生成可预测的输出。

竞态条件

在使用多个线程时,您可能还会遇到“竞态条件”。这是指多个线程尝试同时访问内存中的同一个值的情况。竞态条件会导致出现难以重现且看起来随机的错误,这样的错误可能会导致应用崩溃(通常是不可预测的)。

协程能够处理多任务,但比直接使用线程更为抽象。协程的一项重要功能是能够存储状态,以便协程可以暂停和恢复。协程可以执行,也可以不执行。

借助状态(由“连续性”表示),部分代码可以在需要移交控制权或需要等待其他协程完成后才能恢复时发出信号。此流程称为“协作式多任务处理”。Kotlin 的协程实现增加了一些协助处理多任务的功能。除了连续性以外,创建协程还涉及到作用于 CoroutineScope 内的 Job(具有生命周期的可取消工作单元)的内容。CoroutineScope 表示以递归方式对其子级以及这些子级的子级强制执行取消和其他规则的一种上下文。Dispatcher 会管理协程将使用哪个后备线程来执行任务,从而使开发者无需管理使用新线程的时间和位置。

概念

解释

Job

表示可取消的工作单元,例如使用 launch() 函数创建的工作单元。

CoroutineScope

用于创建新协程的函数,例如 launch()async()CoroutineScope 进行扩展。

Dispatcher调度程序

确定协程将使用的线程。Main 调度程序将始终在主线程上运行协程,而 DefaultIOUnconfined 等调度程序则会使用其他线程。

三、协程

GlobalScope.launch

只要应用在运行,GlobalScope 便允许其中的任何协程运行。鉴于我们讨论的关于主线程的原因,我们不建议在示例代码之外使用这种方法。在应用中使用协程时,我们会使用其他作用域。

launch() 函数会根据括起来的代码(封装在可取消作业对象中)创建协程。launch() 用于无需在协程范围之外返回值的情况。

我们来看一下 launch() 的完整签名,以了解协程中的下一个重要概念。

fun CoroutineScope.launch {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
}

在后台,您传递给 launch 函数的代码块会使用 suspend 关键字进行标记。Suspend 表示代码块或函数可以暂停或恢复。

runBlocking 简述

该函数会启动新协程并在新协程完成之前阻塞当前线程。它主要用于在主要函数和测试中的阻塞代码和非阻塞代码之间架起桥梁。该函数在典型的 Android 代码中并不常用。

import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

val formatter = DateTimeFormatter.ISO_LOCAL_TIME
val time = { formatter.format(LocalDateTime.now()) }

suspend fun getValue(): Double {
    println("entering getValue() at ${time()}")
    delay(3000)
    println("leaving getValue() at ${time()}")
    return Math.random()
}

fun main() {
    runBlocking {
        val num1 = getValue()
        val num2 = getValue()
        println("result of num1 + num2 is ${num1 + num2}")
    }
}

async()

上述的函数会字啊第一个getValue()执行完毕再执行另外一个。而使用async()修饰的函数则会异步操作,在第一个函数等待期间执行其它函数

fun main() {
    runBlocking {
        val num1 = async { getValue() }
        val num2 = async { getValue() }
        println("result of num1 + num2 is ${num1.await() + num2.await()}")
    }
}
Fun CoroutineScope.async() {
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
}: Deferred<T>

async() 函数会返回 Deferred 类型的值。Deferred 是一个可取消的 Job,可以存储对未来值的引用。使用 Deferred,您仍然可以调用函数,就像它会立即返回一个值一样 - Deferred 只充当占位符,因为您无法确定异步任务将何时返回。Deferred(在其他语言中也称为 promise 或 future)能够保证稍后会向此对象返回一个值。另一方面,异步任务默认不会阻塞或等待执行。若要启动异步任务,当前的代码行需要等待 Deferred 的输出,您可以对其调用 await()。它将会返回原始值

何时将函数标记为 suspend

在前面的示例中,您可能已经注意到 getValue() 函数也使用 suspend 关键字进行了定义。原因在于它会调用 delay(),这也是一个 suspend 函数。只要一个函数调用另一个 suspend 函数,那它也应是 suspend 函数。

如果这样的话,为什么我们示例中的 main() 函数不能用 suspend 进行标记呢?毕竟,它也调用 getValue()

不一定。getValue() 实际上是在传递给 runBlocking() 的函数中调用的,这是一个 suspend 函数,类似于传递给 launch()async() 的函数。而 getValue() 不是在 main() 本身中调用的,runBlocking() 也不是 suspend 函数,因此 main() 没有用 suspend 标记。如果函数未调用 suspend 函数,它本身就无需是 suspend 函数。

四、线程的切换

协程是在线程上创建的,因此如果协程创建在主线程上面的话,执行网络请求的话依然会使程序出现错误。所以类似于下文的代码是不正确的

viewModelScope.launch() {
    val jsonBody = "{ username: \"$username\", token: \"$token\"}"
    loginRepository.makeLoginRequest(jsonBody)//执行网络请求
}

因此需要使用Dispatcher调度程序将线程切换到子线程,如下:

viewModelScope.launch(Dispatchers.IO) {//如果不写这个默认就是挂在主线程上面,程序还是会出现异常
    val jsonBody = "{ username: \"$username\", token: \"$token\"}"
    loginRepository.makeLoginRequest(jsonBody)
}

或者withContext(Dispatchers.IO)使用``将makeLoginRequest改为子线程,如下:

withContext(Dispatchers.IO) {
            // Blocking network request code
        }

withContext(Dispatchers.IO)中的代码执行完毕后,程序自动回到原先的线程而无需切换。同时使用withContext(Dispatchers.IO)的函数需要使用挂起suspend来进行修饰。

完整代码如下:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}
private const val loginUrl = "https://www.baidu.com/"
class LoginRepository(private val responseParser: LoginResponseParser) {

    // Function that makes the network request, blocking the current thread
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

五、参考链接

  1. 协程简介
    https://developer.android.google.cn/codelabs/basic-android-kotlin-training-introduction-coroutines
  2. Kotlin协程
    https://developer.android.google.cn/kotlin/coroutines
  3. 协程
    https://kotlinlang.org/docs/coroutines-guide.html