Kotlin学习(十九)—— 携程

协程的引入

注意:kotlin中的协程在1.1中还是实验性的(小编理解为,先不要用)
⼀些 API 启动⻓时间运⾏的操作(例如⽹络 IO、⽂件 IO、CPU 或 GPU 密集型任务等),并要求调⽤者阻塞直到它们完成。协程提供了⼀种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的⽅法:协程挂起

协程通过将复杂性放⼊库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行⼀样简单
上面的语言小编理解为:
1. 我们编程要实现携程,kotlin已经把实现的复杂性转移给一个底层的库
2. 我们实现就像我们写一个Java的同步方法一样,使用一个关键字标记就好
3. 底层的库会帮我们实现协程的调度

许多在其他语言中可用的异步机制可以使⽤ Kotlin 协程实现为库。这包括源于 C# 和 ECMAScript 的 async/await、源于 Go 的 管道 和 select 以及源于 C# 和 Python 生成器/yield。关于提供这些结构的库请参⻅其下⽂描述。

阻塞 vs 挂起

基本上,协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中⼀个会导致⼀些重要的任务被延迟。

另⼀方面,协程挂起几乎是无代价的。不需要上下文切换或者 OS 的任何其他干预。最重要的是,挂起可以在很大程度上由用户库控制:作为库的作者,我们可以决定挂起时发生什么并根据需求优化/记日志/截获。

另⼀个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。
get一下上面的信息:
1. 协程不阻塞,线程会有阻塞状态(Java的线程在等待同步锁,IO,或者调用sleep()方法后等方式进入阻塞,阻塞后的线程有要进入就绪状态才能哟执行的机会,这繁琐的调用方式可能会让任务延迟),而协程是计算可以被挂起(不明白这句话,什么是可以计算被挂起)。
2. 协程不需要上下文切换或者 OS 的任何其他干预,而且用户的控制权大大加大。

挂起函数

当我们调用标记有特殊修饰符 suspend 的函数时,会发生挂起(协程调用可能比较麻烦)
如果我们调用协程,要引入kotlin库:
maven依赖:

<dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-coroutines-core</artifactId>
            <version>0.22.5</version>
</dependency>

我们不能直接调用suspend修饰的方法:
只能从协程和其他挂起函数中调用。(也就是说,suspend修饰的函数,只能被suspend修饰的函数调用)

import kotlinx.coroutines.experimental.async

suspend  fun  doSomething(i : Int):Int{
    println("会被挂起的函数执行了")
    return 1
}

fun main(args: Array<String>) {
  async {  //suspend修饰的很熟
      doSomething(1)
  }
}

注意:上面代码中的async函数是在kotlinx.coroutines.experimental中的
这样的函数(doSomething())称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程,必须至少有⼀个挂起函数,它通常是匿名的(即它是⼀个挂起lambda 表达式)。让我们来看⼀个例⼦,⼀个简化的 async() 函数(源自kotlinx.coroutines 库):
下面贴上async的代码:

public fun <T> async(context: kotlin.coroutines.experimental.CoroutineContext,
                     start: kotlinx.coroutines.experimental.CoroutineStart,
                     parent: kotlinx.coroutines.experimental.Job?,
                     block: suspend kotlinx.coroutines.experimental.CoroutineScope.() -> T): kotlinx.coroutines.experimental.Deferred<T> {
                     }

这⾥的 async() 是⼀个普通函数(不是挂起函数),但是它的 block 参数具有⼀个带 suspend 修饰符的函数类型:suspend kotlinx.coroutines.experimental.CoroutineScope.() -> T 。所以,当我们将⼀个 lambda 表达式传给 async() 时,它会是挂起 lambda 表达式,于是我们可以从中调用挂起函数:

继续该类比,await() 可以是⼀个挂起函数(因此也可以在⼀个 async {} 块中调用),该函数挂起⼀个协程,直到⼀些计算完成并返回其结果:

async {
    ……
    val result = computation.await()
    ……
}

请注意,挂起函数 await() 和 doSomething() 不能在像 main() 这样的普通函数中调⽤:(能作为suspend的参数传入,lambda表达式的函数体中使用)

fun main(args: Array<String>) {
    doSomething() // 错误:挂起函数从⾮协程上下⽂调⽤
}

还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定 suspend 修饰符:

interface Base {
    suspend fun foo()
} 
class Derived: Base {
    override suspend fun foo() { …… }
}

@RestrictsSuspension 注解

扩展函数(和 lambda 表达式)也可以标记为 suspend ,就像普通的⼀样。这允许创建 DSL 及其他用户可扩展的 API。在某些情况下,库作者需要阻止用户添加新方式来挂起协程。
为了实现这⼀点,可以使⽤ @RestrictsSuspension 注解。当接收者类/接口 R 用它标注时,所有挂起扩展都需要委托给 R 的成员或其它委托给它的扩展。由于扩展不能无限相互委托(程序不会终止),这保证所有挂起都通过调⽤ R 的成员发生,库的作者就可以完全控制了。
这在少数情况是需要的,当每次挂起在库中以特殊方式处理时。例如,当通过 buildSequence() 函数实现下⽂所述的生成器时,我们需要确保在协程中的任何挂起调用终调用用yield() 或 yieldAll() 而不是任何其他函数。这就是为什么 SequenceBuilder 用 @RestrictsSuspension 注解:
参⻅其 Github 上 的源代码。

@RestrictsSuspension
public abstract class SequenceBuilder<in T> {
    ……
}

协程的内部机制

我们不是在这⾥给出⼀个关于协程如何⼯作的完整解释,然⽽粗略地认识发⽣了什么是相当重要的。
协程完全通过编译技术实现(不需要来⾃ VM 或 OS 端的⽀持),挂起通过代码来⽣效。基本上,每个挂起函数(优化可能适⽤,但我们不在这⾥讨论)都转换为状态机,其中的状态对应于挂起调⽤。刚好在挂起前,下⼀状态与相关局部变量等⼀起存储在编译器⽣成的类的字段中。在恢复该协程时,恢复局部变量并且状态机从刚好挂起之后的状态进⾏。
挂起的协程可以作为保持其挂起状态与局部变量的对象来存储和传递。这种对象的类型是 Continuation ,⽽这⾥描述的整个代码转换对应于经典的延续性传递⻛格(Continuation-passing style)。因此,挂起函数有⼀个 Continuation 类型的额外参数作为⾼级选项。
关于协程⼯作原理的更多细节可以在这个设计⽂档中找到。在其他语⾔(如 C# 或者 ECMAScript 2016)中的 async/await 的类似描述与此相关,虽然它们实现的语⾔功能可能不像 Kotlin 协程这样通⽤。

协程是实验性的(小编这里先大致了解,今后再探究)

协程的设计是实验性的,这意味着它可能在即将发布的版本中更改。当在 Kotlin 1.1 中编译协程时,默认情况下会报⼀个警告: “协程”功能是实验性的。要移出该警告,你需要指定 opt-in 标志。
由于其实验性状态,标准库中协程相关的 API 放在 kotlin.coroutines.experimental 包下。当设计完成并且实验性状态解除时,最终的 API 会移动到 kotlin.coroutines ,并且实验包会被保留(可能在⼀个单独的构件中)以实现向后兼容。
重要注意事项:我们建议库作者遵循相同惯例:给暴露基于协程 API 的包添加“experimental”后缀(如 com.example.experimental ),以使你的库保持⼆进制兼容。当最终 API 发布时,请按照下列步骤操作:
将所有 API 复制到 com.example(没有 experimental 后缀),保持实验包的向后兼容性。这将最⼩化你的⽤⼾的迁移问题。

展开阅读全文

没有更多推荐了,返回首页