第9章 轻量级线程:协程

在常用的并发模型中,多进程、多线程、分布式是最普遍的,不过近些年来逐渐有一些语言以first-class或者library的形式提供对基于协程的并发模型的支持。其中比较典型的有Scheme、Lua、Python、Perl、Go等以first-class的方式提供对协程的支持。

同样地,Kotlin也支持协程。

本章我们主要介绍:

  • 什么是协程
  • 协程的用法实例
  • 挂起函数
  • 通道与管道
  • 协程的实现原理
  • coroutine库等

9.1 协程简介

从硬件发展来看,从最初的单核单CPU,到单核多CPU,多核多CPU,似乎已经到了极限了,但是单核CPU性能却还在不断提升。如果将程序分为IO密集型应用和CPU密集型应用,二者的发展历程大致如下:

IO密集型应用: 多进程->多线程->事件驱动->协程

CPU密集型应用:多进程–>多线程

如果说多进程对于多CPU,多线程对应多核CPU,那么事件驱动和协程则是在充分挖掘不断提高性能的单核CPU的潜力。

常见的有性能瓶颈的API (例如网络 IO、文件 IO、CPU 或 GPU 密集型任务等),要求调用者阻塞(blocking)直到它们完成才能进行下一步。后来,我们又使用异步回调的方式来实现非阻塞,但是异步回调代码写起来并不简单。

协程提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起。

协程主要是让原来要使用“异步+回调方式”写出来的复杂代码, 简化成可以用看似同步的方式写出来(对线程的操作进一步抽象)。这样我们就可以按串行的思维模型去组织原本分散在不同上下文中的代码逻辑,而不需要去处理复杂的状态同步问题。

协程最早的描述是由Melvin Conway于1958年给出:“subroutines who act as the master program”(与主程序行为类似的子例程)。此后他又在博士论文中给出了如下定义:

  • 数据在后续调用中始终保持( The values of data local to a coroutine persist between successive calls 协程的局部)
  • 当控制流程离开时,协程的执行被挂起,此后控制流程再次进入这个协程时,这个协程只应从上次离开挂起的地方继续 (The execution of a coroutine is suspended as control leaves it, only to carry on where it left off when control re-enters the coroutine at some later stage)。

协程的实现要维护一组局部状态,在重新进入协程前,保证这些状态不被改变,从而能顺利定位到之前的位置。

协程可以用来解决很多问题,比如nodejs的嵌套回调,Erlang以及Golang的并发模型实现等。

实质上,协程(coroutine)是一种用户态的轻量级线程。它由协程构建器(launch coroutine builder)启动。

下面我们通过代码实践来学习协程的相关内容。

9.1.1 搭建协程代码工程

首先,我们来新建一个Kotlin Gradle工程。生成标准gradle工程后,在配置文件build.gradle中,配置kotlinx-coroutines-core依赖:

添加 dependencies :

compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.16'

kotlinx-coroutines还提供了下面的模块:

compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: '0.16'
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-nio', version: '0.16'
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-reactive', version: '0.16'

我们使用Kotlin最新的1.1.3-2 版本:

buildscript {ext.kotlin_version = '1.1.3-2'...dependencies {classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"}
}

其中,kotlin-gradle-plugin是Kotlin集成Gradle的插件。

另外,配置一下JCenter 的仓库:

repositories {jcenter()
}

9.1.2 简单协程示例

下面我们先来看一个简单的协程示例。

运行下面的代码:

    fun firstCoroutineDemo0() {launch(CommonPool) {delay(3000L, TimeUnit.MILLISECONDS)println("Hello,")}println("World!")Thread.sleep(5000L)}

你将会发现输出:

World!
Hello,

上面的这段代码:

launch(CommonPool) {delay(3000L, TimeUnit.MILLISECONDS)println("Hello,")
}

等价于:

launch(CommonPool, CoroutineStart.DEFAULT, {delay(3000L, TimeUnit.MILLISECONDS)println("Hello, ")
})

9.1.3 launch函数

这个launch函数定义在kotlinx.coroutines.experimental下面。

public fun launch(context: CoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> Unit
): Job {val newContext = newCoroutineContext(context)val coroutine = if (start.isLazy)LazyStandaloneCoroutine(newContext, block) elseStandaloneCoroutine(newContext, active = true)coroutine.initParentJob(context[Job])start(block, coroutine, coroutine)return coroutine
}

launch函数有3个入参:context、start、block,这些函数参数分别说明如下:

参数说明
context协程上下文
start协程启动选项
block协程真正要执行的代码块,必须是suspend修饰的挂起函数

这个launch函数返回一个Job类型,Job是协程创建的后台任务的概念,它持有该协程的引用。Job接口实际上继承自CoroutineContext类型。一个Job有如下三种状态:

StateisActiveisCompleted
New (optional initial state) 新建 (可选的初始状态)falsefalse
Active (default initial state) 活动中(默认初始状态)truefalse
Completed (final state) 已结束(最终状态)falsetrue

也就是说,launch函数它以非阻塞(non-blocking)当前线程的方式,启动一个新的协程后台任务,并返回一个Job类型的对象作为当前协程的引用。

另外,这里的delay()函数类似Thread.sleep()的功能,但更好的是:它不会阻塞线程,而只是挂起协程本身。当协程在等待时,线程将返回到池中, 当等待完成时, 协同将在池中的空闲线程上恢复。

9.1.4 CommonPool:共享线程池

我们再来看一下launch(CommonPool) {...}这段代码。

首先,这个CommonPool是代表共享线程池,它的主要作用是来调度计算密集型任务的协程的执行。它的实现使用的是java.util.concurrent包下面的API。它首先尝试创建一个java.util.concurrent.ForkJoinPool (ForkJoinPool是一个可以执行ForkJoinTask的ExcuteService,它采用了work-stealing模式:所有在池中的线程尝试去执行其他线程创建的子任务,这样很少有线程处于空闲状态,更加高效);如果不可用,就使用java.util.concurrent.Executors来创建一个普通的线程池:Executors.newFixedThreadPool。相关代码在kotlinx/coroutines/experimental/CommonPool.kt中:

    private fun createPool(): ExecutorService {val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }?: return createPlainPool()if (!usePrivatePool) {Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }?.let { return it }}Try { fjpClass.getConstructor(Int::class.java).newInstance(defaultParallelism()) as? ExecutorService }?. let { return it }return createPlainPool()}private fun createPlainPool(): ExecutorService {val threadId = AtomicInteger()return Executors.newFixedThreadPool(defaultParallelism()) {Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }}}

这个CommonPool对象类是CoroutineContext的子类型。它们的类型集成层次结构如下:

螢幕快照 2017-07-12 13.14.54.png

9.1.5 挂起函数

代码块中的delay(3000L, TimeUnit.MILLISECONDS)函数,是一个用suspend关键字修饰的函数,我们称之为挂起函数。挂起函数只能从协程代码内部调用,普通的非协程的代码不能调用。

挂起函数只允许由协程或者另外一个挂起函数里面调用, 例如我们在协程代码中调用一个挂起函数,代码示例如下:

    suspend fun runCoroutineDemo() {run(CommonPool) {delay(3000L, TimeUnit.MILLISECONDS)println("suspend,")}println("runCoroutineDemo!")Thread.sleep(5000L)}fun callSuspendFun() {launch(CommonPool) {runCoroutineDemo()}}

如果我们用Java中的Thread类来写类似功能的代码,上面的代码可以写成这样:

    fun threadDemo0() {Thread({Thread.sleep(3000L)println("Hello,")}).start()println("World!")Thread.sleep(5000L)}

输出结果也是:

World!
Hello,

另外, 我们不能使用Thread来启动协程代码。例如下面的写法编译器会报错:

    /*** 错误反例:用线程调用协程 error*/fun threadCoroutineDemo() {Thread({delay(3000L, TimeUnit.MILLISECONDS) // error, Suspend functions are only allowed to be called from a coroutine or another suspend functionprintln("Hello,")})println("World!")Thread.sleep(5000L)}

9.2 桥接 阻塞和非阻塞

上面的例子中,我们给出的是使用非阻塞的delay函数,同时有使用了阻塞的Thread.sleep函数,这样代码写在一起可读性不是那么地好。让我们来使用纯的Kotlin的协程代码来实现上面的 阻塞+非阻塞 的例子(不用Thread)。

9.2.1 runBlocking函数

Kotlin中提供了runBlocking函数来实现类似主协程的功能:

fun main(args: Array<String>) = runBlocking<Unit> {// 主协程println("${format(Date())}: T0")// 启动主协程launch(CommonPool) {//在common thread pool中创建协程println("${format(Date())}: T1")delay(3000L)println("${format(Date())}: T2 Hello,")}println("${format(Date())}: T3 World!") //  当子协程被delay,主协程仍然继续运行delay(5000L)println("${format(Date())}: T4")
}

运行结果:

14:37:59.640: T0
14:37:59.721: T1
14:37:59.721: T3 World!
14:38:02.763: T2 Hello,
14:38:04.738: T4

可以发现,运行结果跟之前的是一样的,但是我们没有使用Thread.sleep,我们只使用了非阻塞的delay函数。如果main函数不加 = runBlocking<Unit> , 那么我们是不能在main函数体内调用delay(5000L)的。

如果这个阻塞的线程被中断,runBlocking抛出InterruptedException异常。

该runBlocking函数不是用来当做普通协程函数使用的,它的设计主要是用来桥接普通阻塞代码和挂起风格的(suspending style)的非阻塞代码的, 例如用在 main 函数中,或者用于测试用例代码中。

@RunWith(JUnit4::class)
class RunBlockingTest {@Test fun testRunBlocking() = runBlocking<Unit> {// 这样我们就可以在这里调用任何suspend fun了launch(CommonPool) {delay(3000L)}delay(5000L)}
}

9.3 等待一个任务执行完毕

我们先来看一段代码:

    fun firstCoroutineDemo() {launch(CommonPool) {delay(3000L, TimeUnit.MILLISECONDS)println("[firstCoroutineDemo] Hello, 1")}launch(CommonPool, CoroutineStart.DEFAULT, {delay(3000L, TimeUnit.MILLISECONDS)println("[firstCoroutineDemo] Hello, 2")})println("[firstCoroutineDemo] World!")}

运行这段代码,我们会发现只输出:

[firstCoroutineDemo] World!

这是为什么?

为了弄清上面的代码执行的内部过程,我们打印一些日志看下:

   fun testJoinCoroutine() = runBlocking<Unit> {// Start a coroutineval c1 = launch(CommonPool) {println("C1 Thread: ${Thread.currentThread()}")println("C1 Start")delay(3000L)println("C1 World! 1")}val c2 = launch(CommonPool) {println("C2 Thread: ${Thread.currentThread()}")println("C2 Start")delay(5000L)println("C2 World! 2")}println("Main Thread: ${Thread.currentThread()}")println("Hello,")println("Hi,")println("c1 is active: ${c1.isActive}  ${c1.isCompleted}")println("c2 is active: ${c2.isActive}  ${c2.isCompleted}")}

再次运行:

C1 Thread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
C1 Start
C2 Thread: Thread[ForkJoinPool.commonPool-worker-2,5,main]
C2 Start
Main Thread: Thread[main,5,main]
Hello,
Hi,
c1 is active: true  false
c2 is active: true  false

我们可以看到,这里的C1、C2代码也开始执行了,使用的是ForkJoinPool.commonPool-worker线程池中的worker线程。但是,我们在代码执行到最后打印出这两个协程的状态isCompleted都是false,这表明我们的C1、C2的代码,在Main Thread结束的时刻(此时的运行main函数的Java进程也退出了),还没有执行完毕,然后就跟着主线程一起退出结束了。

所以我们可以得出结论:运行 main () 函数的主线程, 必须要等到我们的协程完成之前结束 , 否则我们的程序在 打印Hello, 1和Hello, 2之前就直接结束掉了。

我们怎样让这两个协程参与到主线程的时间顺序里呢?我们可以使用join, 让主线程一直等到当前协程执行完毕再结束, 例如下面的这段代码

    fun testJoinCoroutine() = runBlocking<Unit> {// Start a coroutineval c1 = launch(CommonPool) {println("C1 Thread: ${Thread.currentThread()}")println("C1 Start")delay(3000L)println("C1 World! 1")}val c2 = launch(CommonPool) {println("C2 Thread: ${Thread.currentThread()}")println("C2 Start")delay(5000L)println("C2 World! 2")}println("Main Thread: ${Thread.currentThread()}")println("Hello,")println("c1 is active: ${c1.isActive}  isCompleted: ${c1.isCompleted}")println("c2 is active: ${c2.isActive}  isCompleted: ${c2.isCompleted}")c1.join() // the main thread will wait until child coroutine completesprintln("Hi,")println("c1 is active: ${c1.isActive}  isCompleted: ${c1.isCompleted}")println("c2 is active: ${c2.isActive}  isCompleted: ${c2.isCompleted}")c2.join() // the main thread will wait until child coroutine completesprintln("c1 is active: ${c1.isActive}  isCompleted: ${c1.isCompleted}")println("c2 is active: ${c2.isActive}  isCompleted: ${c2.isCompleted}")}

将会输出:

C1 Thread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
C1 Start
C2 Thread: Thread[ForkJoinPool.commonPool-worker-2,5,main]
C2 Start
Main Thread: Thread[main,5,main]
Hello,
c1 is active: true  isCompleted: false
c2 is active: true  isCompleted: false
C1 World! 1
Hi,
c1 is active: false  isCompleted: true
c2 is active: true  isCompleted: false
C2 World! 2
c1 is active: false  isCompleted: true
c2 is active: false  isCompleted: true

通常,良好的代码风格我们会把一个单独的逻辑放到一个独立的函数中,我们可以重构上面的代码如下:

    fun testJoinCoroutine2() = runBlocking<Unit> {// Start a coroutineval c1 = launch(CommonPool) {fc1()}val c2 = launch(CommonPool) {fc2()}...}private suspend fun fc2() {println("C2 Thread: ${Thread.currentThread()}")println("C2 Start")delay(5000L)println("C2 World! 2")}private suspend fun fc1() {println("C1 Thread: ${Thread.currentThread()}")println("C1 Start")delay(3000L)println("C1 World! 1")}

可以看出,我们这里的fc1, fc2函数是suspend fun。

9.4 协程是轻量级的

直接运行下面的代码:

    fun testThread() {val jobs = List(100_1000) {Thread({Thread.sleep(1000L)print(".")})}jobs.forEach { it.start() }jobs.forEach { it.join() }}

我们应该会看到输出报错:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native threadat java.lang.Thread.start0(Native Method)at java.lang.Thread.start(Thread.java:714)at com.easy.kotlin.LightWeightCoroutinesDemo.testThread(LightWeightCoroutinesDemo.kt:30)at com.easy.kotlin.LightWeightCoroutinesDemoKt.main(LightWeightCoroutinesDemo.kt:40)
...........................................................................................

我们这里直接启动了100,000个线程,并join到一起打印".", 不出意外的我们收到了java.lang.OutOfMemoryError

这个异常问题本质原因是我们创建了太多的线程,而能创建的线程数是有限制的,导致了异常的发生。在Java中, 当我们创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。 能创建的线程数的具体计算公式如下:

Number of Threads = (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize)

其中,参数说明如下:

参数说明
MaxProcessMemory指的是一个进程的最大内存
JVMMemoryJVM内存
ReservedOsMemory保留的操作系统内存
ThreadStackSize线程栈的大小

我们通常在优化这种问题的时候,要么是采用减小thread stack的大小的方法,要么是采用减小heap或permgen初始分配的大小方法等方式来临时解决问题。

在协程中,情况完全就不一样了。我们看一下实现上面的逻辑的协程代码:

    fun testLightWeightCoroutine() = runBlocking {val jobs = List(100_000) {// create a lot of coroutines and list their jobslaunch(CommonPool) {delay(1000L)print(".")}}jobs.forEach { it.join() } // wait for all jobs to complete}

运行上面的代码,我们将看到输出:

START: 21:22:28.913
.....................
.....................(100000个)
.....END: 21:22:30.956

上面的程序在2s左右的时间内正确执行完毕。

9.5 协程 vs 守护线程

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。

所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。

我们来看一段Thread的守护线程的代码:

    fun testDaemon2() {val t = Thread({repeat(100) { i ->println("I'm sleeping $i ...")Thread.sleep(500L)}})t.isDaemon = true // 必须在启动线程前调用,否则会报错:Exception in thread "main" java.lang.IllegalThreadStateExceptiont.start()Thread.sleep(2000L) // just quit after delay}

这段代码启动一个线程,并设置为守护线程。线程内部是间隔500ms 重复打印100次输出。外部主线程睡眠2s。

运行这段代码,将会输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm sleeping 3 ...

协程跟守护线程很像,用协程来写上面的逻辑,代码如下:

    fun testDaemon1() = runBlocking {launch(CommonPool) {repeat(100) { i ->println("I'm sleeping $i ...")delay(500L)}}delay(2000L) // just quit after delay}

运行这段代码,我们发现也输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm sleeping 3 ...

我们可以看出,活动的协程不会使进程保持活动状态。它们的行为就像守护程序线程。

9.6 协程执行的取消

我们知道,启动函数launch返回一个Job引用当前协程,该Job引用可用于取消正在运行协程:

    fun testCancellation() = runBlocking<Unit> {val job = launch(CommonPool) {repeat(1000) { i ->println("I'm sleeping $i ... CurrentThread: ${Thread.currentThread()}")delay(500L)}}delay(1300L)println("CurrentThread: ${Thread.currentThread()}")println("Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")val b1 = job.cancel() // cancels the jobprintln("job cancel: $b1")delay(1300L)println("Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")val b2 = job.cancel() // cancels the job, job already canceld, return falseprintln("job cancel: $b2")println("main: Now I can quit.")}

运行上面的代码,将会输出:

I'm sleeping 0 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 1 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 2 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
CurrentThread: Thread[main,5,main]
Job is alive: true  Job is completed: false
job cancel: true
Job is alive: false  Job is completed: true
job cancel: false
main: Now I can quit.

我们可以看出,当job还在运行时,isAlive是true,isCompleted是false。当调用job.cancel取消该协程任务,cancel函数本身返回true, 此时协程的打印动作就停止了。此时,job的状态是isAlive是false,isCompleted是true。 如果,再次调用job.cancel函数,我们将会看到cancel函数返回的是false。

9.6.1 计算代码的协程取消失效

kotlinx 协程的所有suspend函数都是可以取消的。我们可以通过job的isActive状态来判断协程的状态,或者检查手否有抛出 CancellationException 时取消。

例如,协程正工作在循环计算中,并且不检查协程当前的状态, 那么调用cancel来取消协程将无法停止协程的运行, 如下面的示例所示:

    fun testCooperativeCancellation1() = runBlocking<Unit> {val job = launch(CommonPool) {var nextPrintTime = 0Lvar i = 0while (i < 20) { // computation loopval currentTime = System.currentTimeMillis()if (currentTime >= nextPrintTime) {println("I'm sleeping ${i++} ... CurrentThread: ${Thread.currentThread()}")nextPrintTime = currentTime + 500L}}}delay(3000L)println("CurrentThread: ${Thread.currentThread()}")println("Before cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")val b1 = job.cancel() // cancels the jobprintln("job cancel1: $b1")println("After Cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")delay(30000L)val b2 = job.cancel() // cancels the job, job already canceld, return falseprintln("job cancel2: $b2")println("main: Now I can quit.")}

运行上面的代码,输出:

I'm sleeping 0 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 1 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
...
I'm sleeping 6 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
CurrentThread: Thread[main,5,main]
Before cancel, Job is alive: true  Job is completed: false
job cancel1: true
After Cancel, Job is alive: false  Job is completed: true
I'm sleeping 7 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
...
I'm sleeping 18 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 19 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
job cancel2: false
main: Now I can quit.

我们可以看出,即使我们调用了cancel函数,当前的job状态isAlive是false了,但是协程的代码依然一直在运行,并没有停止。

9.6.2 计算代码协程的有效取消

有两种方法可以使计算代码取消成功。

方法一: 显式检查取消状态isActive

我们直接给出实现的代码:

fun testCooperativeCancellation2() = runBlocking<Unit> {val job = launch(CommonPool) {var nextPrintTime = 0Lvar i = 0while (i < 20) { // computation loopif (!isActive) {return@launch}val currentTime = System.currentTimeMillis()if (currentTime >= nextPrintTime) {println("I'm sleeping ${i++} ... CurrentThread: ${Thread.currentThread()}")nextPrintTime = currentTime + 500L}}}delay(3000L)println("CurrentThread: ${Thread.currentThread()}")println("Before cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")val b1 = job.cancel() // cancels the jobprintln("job cancel1: $b1")println("After Cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")delay(3000L)val b2 = job.cancel() // cancels the job, job already canceld, return falseprintln("job cancel2: $b2")println("main: Now I can quit.")}

运行这段代码,输出:

I'm sleeping 0 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 1 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 2 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 3 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 4 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 5 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 6 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
CurrentThread: Thread[main,5,main]
Before cancel, Job is alive: true  Job is completed: false
job cancel1: true
After Cancel, Job is alive: false  Job is completed: true
job cancel2: false
main: Now I can quit.

正如您所看到的, 现在这个循环可以被取消了。这里的isActive属性是CoroutineScope中的属性。这个接口的定义是:

public interface CoroutineScope {public val isActive: Booleanpublic val context: CoroutineContext
}

该接口用于通用协程构建器的接收器,以便协程中的代码可以方便的访问其isActive状态值(取消状态),以及其上下文CoroutineContext信息。

方法二: 循环调用一个挂起函数yield()

该方法实质上是通过job的isCompleted状态值来捕获CancellationException完成取消功能。

我们只需要在while循环体中循环调用yield()来检查该job的取消状态,如果已经被取消,那么isCompleted值将会是true,yield函数就直接抛出CancellationException异常,从而完成取消的功能:

        val job = launch(CommonPool) {var nextPrintTime = 0Lvar i = 0while (i < 20) { // computation loopyield()val currentTime = System.currentTimeMillis()if (currentTime >= nextPrintTime) {println("I'm sleeping ${i++} ... CurrentThread: ${Thread.currentThread()}")nextPrintTime = currentTime + 500L}}}

运行上面的代码,输出:

I'm sleeping 0 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-1,5,main]
I'm sleeping 1 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-2,5,main]
I'm sleeping 2 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-2,5,main]
I'm sleeping 3 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-3,5,main]
I'm sleeping 4 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-3,5,main]
I'm sleeping 5 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-3,5,main]
I'm sleeping 6 ... CurrentThread: Thread[ForkJoinPool.commonPool-worker-2,5,main]
CurrentThread: Thread[main,5,main]
Before cancel, Job is alive: true  Job is completed: false
job cancel1: true
After Cancel, Job is alive: false  Job is completed: true
job cancel2: false
main: Now I can quit.

如果我们想看看yield函数抛出的异常,我们可以加上try catch打印出日志:

try {yield()
} catch (e: Exception) {println("$i ${e.message}")
}

我们可以看到类似:Job was cancelled 这样的信息。

这个yield函数的实现是:

suspend fun yield(): Unit = suspendCoroutineOrReturn sc@ { cont ->val context = cont.contextval job = context[Job]if (job != null && job.isCompleted) throw job.getCompletionException()if (cont !is DispatchedContinuation<Unit>) return@sc Unitif (!cont.dispatcher.isDispatchNeeded(context)) return@sc Unitcont.dispatchYield(job, Unit)COROUTINE_SUSPENDED
}

如果调用此挂起函数时,当前协程的Job已经完成 (isActive = false, isCompleted = true),当前协程将以CancellationException取消。

9.6.3 在finally中的协程代码

当我们取消一个协程任务时,如果有try {...} finally {...}代码块,那么finally {…}中的代码会被正常执行完毕:

    fun finallyCancelDemo() = runBlocking {val job = launch(CommonPool) {try {repeat(1000) { i ->println("I'm sleeping $i ...")delay(500L)}} finally {println("I'm running finally")}}delay(2000L)println("Before cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")job.cancel()println("After cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")delay(2000L)println("main: Now I can quit.")}

运行这段代码,输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm sleeping 3 ...
Before cancel, Job is alive: true  Job is completed: false
I'm running finally
After cancel, Job is alive: false  Job is completed: true
main: Now I can quit.

我们可以看出,在调用cancel之后,就算当前协程任务Job已经结束了,finally{...}中的代码依然被正常执行。

但是,如果我们在finally{...}中放入挂起函数:

    fun finallyCancelDemo() = runBlocking {val job = launch(CommonPool) {try {repeat(1000) { i ->println("I'm sleeping $i ...")delay(500L)}} finally {println("I'm running finally")delay(1000L)println("And I've delayed for 1 sec ?")}}delay(2000L)println("Before cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")job.cancel()println("After cancel, Job is alive: ${job.isActive}  Job is completed: ${job.isCompleted}")delay(2000L)println("main: Now I can quit.")}

运行上述代码,我们将会发现只输出了一句:I’m running finally。因为主线程在挂起函数delay(1000L)以及后面的打印逻辑还没执行完,就已经结束退出。

            } finally {println("I'm running finally")delay(1000L)println("And I've delayed for 1 sec ?")}

9.6.4 协程执行不可取消的代码块

如果我们想要上面的例子中的finally{...}完整执行,不被取消函数操作所影响,我们可以使用 run 函数和 NonCancellable 上下文将相应的代码包装在 run (NonCancellable) {…} 中, 如下面的示例所示:

    fun testNonCancellable() = runBlocking {val job = launch(CommonPool) {try {repeat(1000) { i ->println("I'm sleeping $i ...")delay(500L)}} finally {run(NonCancellable) {println("I'm running finally")delay(1000L)println("And I've just delayed for 1 sec because I'm non-cancellable")}}}delay(2000L)println("main: I'm tired of waiting!")job.cancel()delay(2000L)println("main: Now I can quit.")}

运行输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm sleeping 3 ...
main: I'm tired of waiting!
I'm running finally
And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.

9.7 设置协程超时时间

我们通常取消协同执行的原因给协程的执行时间设定一个执行时间上限。我们也可以使用 withTimeout 函数来给一个协程任务的执行设定最大执行时间,超出这个时间,就直接终止掉。代码示例如下:

    fun testTimeouts() = runBlocking {withTimeout(3000L) {repeat(100) { i ->println("I'm sleeping $i ...")delay(500L)}}}

运行上述代码,我们将会看到如下输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm sleeping 3 ...
I'm sleeping 4 ...
I'm sleeping 5 ...
Exception in thread "main" kotlinx.coroutines.experimental.TimeoutException: Timed out waiting for 3000 MILLISECONDSat kotlinx.coroutines.experimental.TimeoutExceptionCoroutine.run(Scheduled.kt:110)at kotlinx.coroutines.experimental.EventLoopImpl$DelayedRunnableTask.invoke(EventLoop.kt:199)at kotlinx.coroutines.experimental.EventLoopImpl$DelayedRunnableTask.invoke(EventLoop.kt:195)at kotlinx.coroutines.experimental.EventLoopImpl.processNextEvent(EventLoop.kt:111)at kotlinx.coroutines.experimental.BlockingCoroutine.joinBlocking(Builders.kt:205)at kotlinx.coroutines.experimental.BuildersKt.runBlocking(Builders.kt:150)at kotlinx.coroutines.experimental.BuildersKt.runBlocking$default(Builders.kt:142)at com.easy.kotlin.CancellingCoroutineDemo.testTimeouts(CancellingCoroutineDemo.kt:169)at com.easy.kotlin.CancellingCoroutineDemoKt.main(CancellingCoroutineDemo.kt:193)

由 withTimeout 抛出的 TimeoutException 是 CancellationException 的一个子类。这个TimeoutException类型定义如下:

private class TimeoutException(time: Long,unit: TimeUnit,@JvmField val coroutine: Job
) : CancellationException("Timed out waiting for $time $unit")

如果您需要在超时时执行一些附加操作, 则可以把逻辑放在 try {…} catch (e: CancellationException) {…} 代码块中。例如:

    try {ccd.testTimeouts()} catch (e: CancellationException) {println("I am timed out!")}

9.8 挂起函数的组合执行

本节我们介绍挂起函数组合的各种方法。

9.8.1 按默认顺序执行

假设我们有两个在别处定义的挂起函数:

    suspend fun doJob1(): Int {println("Doing Job1 ...")delay(1000L) // 此处模拟我们的工作代码println("Job1 Done")return 10}suspend fun doJob2(): Int {println("Doing Job2 ...")delay(1000L) // 此处模拟我们的工作代码println("Job2 Done")return 20}

如果需要依次调用它们, 我们只需要使用正常的顺序调用, 因为协同中的代码 (就像在常规代码中一样) 是默认的顺序执行。下面的示例通过测量执行两个挂起函数所需的总时间来演示:

    fun testSequential() = runBlocking<Unit> {val time = measureTimeMillis {val one = doJob1()val two = doJob2()println("[testSequential] 最终结果: ${one + two}")}println("[testSequential] Completed in $time ms")}

执行上面的代码,我们将得到输出:

Doing Job1 ...
Job1 Done
Doing Job2 ...
Job2 Done
[testSequential] 最终结果: 30
[testSequential] Completed in 6023 ms

可以看出,我们的代码是跟普通的代码一样顺序执行下去。

9.8.2 使用async异步并发执行

上面的例子中,如果在调用 doJob1 和 doJob2 之间没有时序上的依赖关系, 并且我们希望通过同时并发地执行这两个函数来更快地得到答案, 那该怎么办呢?这个时候,我们就可以使用async来实现异步。代码示例如下:

    fun testAsync() = runBlocking<Unit> {val time = measureTimeMillis {val one = async(CommonPool) { doJob1() }val two = async(CommonPool) { doJob2() }println("最终结果: ${one.await() + two.await()}")}println("Completed in $time ms")}

如果跟上面同步的代码一起执行对比,我们可以看到如下输出:

Doing Job1 ...
Job1 Done
Doing Job2 ...
Job2 Done
[testSequential] 最终结果: 30
[testSequential] Completed in 6023 ms
Doing Job1 ...
Doing Job2 ...
Job1 Done
Job2 Done
[testAsync] 最终结果: 30
[testAsync] Completed in 3032 ms

我们可以看出,使用async函数,我们的两个Job并发的执行了,并发花的时间要比顺序的执行的要快将近两倍。因为,我们有两个任务在并发的执行。

从概念上讲, async跟launch类似, 它启动一个协程, 它与其他协程并发地执行。

不同之处在于, launch返回一个任务Job对象, 不带任何结果值;而async返回一个延迟任务对象Deferred,一种轻量级的非阻塞性future, 它表示后面会提供结果。

在上面的示例代码中,我们使用Deferred调用 await() 函数来获得其最终结果。另外,延迟任务Deferred也是Job类型, 它继承自Job,所以它也有isActive、isCompleted属性,也有join()、cancel()函数,因此我们也可以在需要时取消它。Deferred接口定义如下:

public interface Deferred<out T> : Job {val isCompletedExceptionally: Booleanval isCancelled: Booleanpublic suspend fun await(): Tpublic fun <R> registerSelectAwait(select: SelectInstance<R>, block: suspend (T) -> R)public fun getCompleted(): T@Deprecated(message = "Use `isActive`", replaceWith = ReplaceWith("isActive"))public val isComputing: Boolean get() = isActive
}

其中,常用的属性和函数说明如下:

名称说明
isCompletedExceptionally当协程在计算过程中有异常failed 或被取消,返回true。 这也意味着isActive等于 false ,同时 isCompleted等于 true
isCancelled如果当前延迟任务被取消,返回true
suspend fun await()等待此延迟任务完成,而不阻塞线程;如果延迟任务完成, 则返回结果值或引发相应的异常。

延迟任务对象Deferred的状态与对应的属性值如下表所示:

状态isActiveisCompletedisCompletedExceptionallyisCancelled
New (可选初始状态)falsefalsefalsefalse
Active (默认初始状态)truefalsefalsefalse
Resolved (最终状态)falsetruefalsefalse
Failed (最终状态)falsetruetruefalse
Cancelled (最终状态)falsetruetruetrue

9.9 协程上下文与调度器

到这里,我们已经看到了下面这些启动协程的方式:

launch(CommonPool) {...}
async(CommonPool) {...}
run(NonCancellable) {...}

这里的CommonPool 和 NonCancellable 是协程上下文(coroutine contexts)。本小节我们简单介绍一下自定义协程上下文。

9.9.1 调度和线程

协程上下文包括一个协程调度程序, 它可以指定由哪个线程来执行协程。调度器可以将协程的执行调度到一个线程池,限制在特定的线程中;也可以不作任何限制,让它无约束地运行。请看下面的示例:

    fun testDispatchersAndThreads() = runBlocking {val jobs = arrayListOf<Job>()jobs += launch(Unconfined) {// 未作限制 -- 将会在 main thread 中执行println("Unconfined: I'm working in thread ${Thread.currentThread()}")}jobs += launch(context) {// 父协程的上下文 : runBlocking coroutineprintln("context: I'm working in thread ${Thread.currentThread()}")}jobs += launch(CommonPool) {// 调度指派给 ForkJoinPool.commonPoolprintln("CommonPool: I'm working in thread ${Thread.currentThread()}")}jobs += launch(newSingleThreadContext("MyOwnThread")) {// 将会在这个协程自己的新线程中执行println("newSingleThreadContext: I'm working in thread ${Thread.currentThread()}")}jobs.forEach { it.join() }}

运行上面的代码,我们将得到以下输出 (可能按不同的顺序):

Unconfined: I'm working in thread Thread[main,5,main]
CommonPool: I'm working in thread Thread[ForkJoinPool.commonPool-worker-1,5,main]
newSingleThreadContext: I'm working in thread Thread[MyOwnThread,5,main]
context: I'm working in thread Thread[main,5,main]

从上面的结果,我们可以看出:
使用无限制的Unconfined上下文的协程运行在主线程中;
继承了 runBlocking {…} 的context的协程继续在主线程中执行;
而CommonPool在ForkJoinPool.commonPool中;
我们使用newSingleThreadContext函数新建的协程上下文,该协程运行在自己的新线程Thread[MyOwnThread,5,main]中。

另外,我们还可以在使用 runBlocking的时候显式指定上下文, 同时使用 run 函数来更改协程的上下文:

    fun log(msg: String) = println("${Thread.currentThread()} $msg")fun testRunBlockingWithSpecifiedContext() = runBlocking {log("$context")log("${context[Job]}")log("开始")val ctx1 = newSingleThreadContext("线程A")val ctx2 = newSingleThreadContext("线程B")runBlocking(ctx1) {log("Started in Context1")run(ctx2) {log("Working in Context2")}log("Back to Context1")}log("结束")}

运行输出:

Thread[main,5,main] [BlockingCoroutine{Active}@b1bc7ed, EventLoopImpl@7cd84586]
Thread[main,5,main] BlockingCoroutine{Active}@b1bc7ed
Thread[main,5,main] 开始
Thread[线程A,5,main] Started in Context1
Thread[线程B,5,main] Working in Context2
Thread[线程A,5,main] Back to Context1
Thread[main,5,main] 结束

9.9.2 父子协程

当我们使用协程A的上下文启动另一个协程B时, B将成为A的子协程。当父协程A任务被取消时, B以及它的所有子协程都会被递归地取消。代码示例如下:

    fun testChildrenCoroutine()= runBlocking<Unit> {val request = launch(CommonPool) {log("ContextA1: ${context}")val job1 = launch(CommonPool) {println("job1: 独立的协程上下文!")delay(1000)println("job1: 不会受到request.cancel()的影响")}// 继承父上下文:request的contextval job2 = launch(context) {log("ContextA2: ${context}")println("job2: 是request coroutine的子协程")delay(1000)println("job2: 当request.cancel(),job2也会被取消")}job1.join()job2.join()}delay(500)request.cancel()delay(1000)println("main: Who has survived request cancellation?")}

运行输出:

Thread[ForkJoinPool.commonPool-worker-1,5,main] ContextA1: [StandaloneCoroutine{Active}@5b646af2, CommonPool]
job1: 独立的协程上下文!
Thread[ForkJoinPool.commonPool-worker-3,5,main] ContextA2: [StandaloneCoroutine{Active}@75152aa4, CommonPool]
job2: 是request coroutine的子协程
job1: 不会受到request.cancel()的影响
main: Who has survived request cancellation?

9.10 通道

延迟对象提供了一种在协程之间传输单个值的方法。而通道(Channel)提供了一种传输数据流的方法。通道是使用 SendChannel 和使用 ReceiveChannel 之间的非阻塞通信。

9.10.1 通道 vs 阻塞队列

通道的概念类似于 阻塞队列(BlockingQueue)。在Java的Concurrent包中,BlockingQueue很好的解决了多线程中如何高效安全“传输”数据的问题。它有两个常用的方法如下:

  • E take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空, 阻塞进入等待状态直到BlockingQueue有新的数据被加入;

  • put(E e): 把对象 e 加到BlockingQueue里, 如果BlockQueue没有空间,则调用此方法的线程被阻塞,直到BlockingQueue里面有空间再继续。

通道跟阻塞队列一个关键的区别是:通道有挂起的操作, 而不是阻塞的, 同时它可以关闭。

代码示例:

package com.easy.kotlinimport kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking/*** Created by jack on 2017/7/13.*/
class ChannelsDemo {fun testChannel() = runBlocking<Unit> {val channel = Channel<Int>()launch(CommonPool) {for (x in 1..10) channel.send(x * x)}println("channel = ${channel}")// here we print five received integers:repeat(10) { println(channel.receive()) }println("Done!")}
}fun main(args: Array<String>) {val cd = ChannelsDemo()cd.testChannel()
}

运行输出:

channel = kotlinx.coroutines.experimental.channels.RendezvousChannel@2e817b38
1
4
9
16
25
36
49
64
81
100
Done!

我们可以看出使用Channel<Int>()背后调用的是会合通道RendezvousChannel(),会合通道中没有任何缓冲区。send函数被挂起直到另外一个协程调用receive函数, 然后receive函数挂起直到另外一个协程调用send函数。它是一个完全无锁的实现。

9.10.2 关闭通道和迭代遍历元素

与队列不同, 通道可以关闭, 以指示没有更多的元素。在接收端, 可以使用 for 循环从通道接收元素。代码示例:

    fun testClosingAndIterationChannels() = runBlocking {val channel = Channel<Int>()launch(CommonPool) {for (x in 1..5) channel.send(x * x)channel.close() // 我们结束 sending}// 打印通道中的值,直到通道关闭for (x in channel) println(x)println("Done!")}

其中, close函数在这个通道上发送一个特殊的 “关闭令牌”。这是一个幂等运算:对此函数的重复调用不起作用, 并返回 “false”。此函数执行后,isClosedForSend返回 “true”。但是, ReceiveChannelisClosedForReceive在所有之前发送的元素收到之后才返回 “true”。

我们把上面的代码加入打印日志:

    fun testClosingAndIterationChannels() = runBlocking {val channel = Channel<Int>()launch(CommonPool) {for (x in 1..5) {channel.send(x * x)}println("Before Close => isClosedForSend = ${channel.isClosedForSend}")channel.close() // 我们结束 sendingprintln("After Close => isClosedForSend = ${channel.isClosedForSend}")}// 打印通道中的值,直到通道关闭for (x in channel) {println("${x} => isClosedForReceive = ${channel.isClosedForReceive}")}println("Done!  => isClosedForReceive = ${channel.isClosedForReceive}")}

运行输出:

1 => isClosedForReceive = false
4 => isClosedForReceive = false
9 => isClosedForReceive = false
16 => isClosedForReceive = false
25 => isClosedForReceive = false
Before Close => isClosedForSend = false
After Close => isClosedForSend = true
Done!  => isClosedForReceive = true

9.10.3 生产者-消费者模式

使用协程生成元素序列的模式非常常见。这是在并发代码中经常有的生产者-消费者模式。代码示例:

    fun produceSquares() = produce<Int>(CommonPool) {for (x in 1..7) send(x * x)}fun consumeSquares() = runBlocking{val squares = produceSquares()squares.consumeEach { println(it) }println("Done!")}

这里的produce函数定义如下:

public fun <E> produce(context: CoroutineContext,capacity: Int = 0,block: suspend ProducerScope<E>.() -> Unit
): ProducerJob<E> {val channel = Channel<E>(capacity)return ProducerCoroutine(newCoroutineContext(context), channel).apply {initParentJob(context[Job])block.startCoroutine(this, this)}
}

其中,参数说明如下:

参数名说明
context协程上下文
capacity通道缓存容量大小 (默认没有缓存)
block协程代码块

produce函数会启动一个新的协程, 协程中发送数据到通道来生成数据流,并以 ProducerJob对象返回对协程的引用。ProducerJob继承了Job, ReceiveChannel类型。

9.11 管道

9.11.1 生产无限序列

管道(Pipeline)是一种模式, 我们可以用一个协程生产无限序列:

    fun produceNumbers() = produce<Long>(CommonPool) {var x = 1Lwhile (true) send(x++) // infinite stream of integers starting from 1}

我们的消费序列的函数如下:

    fun produceNumbers() = produce<Long>(CommonPool) {var x = 1Lwhile (true) send(x++) // infinite stream of integers starting from 1}

主代码启动并连接整个管线:

fun testPipeline() = runBlocking {val numbers = produceNumbers() // produces integers from 1 and onval squares = consumeNumbers(numbers) // squares integers//for (i in 1..6) println(squares.receive())while (true) {println(squares.receive())}println("Done!")squares.cancel()numbers.cancel()}

运行上面的代码,我们将会发现控制台在打印一个无限序列,完全没有停止的意思。

9.11.2 管道与无穷质数序列

我们使用协程管道来生成一个无穷质数序列。

我们从无穷大的自然数序列开始:

    fun numbersProducer(context: CoroutineContext, start: Int) = produce<Int>(context) {var n = startwhile (true) send(n++) // infinite stream of integers from start}

这次我们引入一个显式上下文参数context, 以便调用方可以控制我们的协程运行的位置。

下面的管道将筛选传入的数字流, 过滤掉可以被当前质数整除的所有数字:

    fun filterPrimes(context: CoroutineContext, numbers: ReceiveChannel<Int>, prime: Int) = produce<Int>(context) {for (x in numbers) if (x % prime != 0) send(x)}

现在我们通过从2开始, 从当前通道中取一个质数, 并为找到的每个质数启动新的管道阶段, 从而构建出我们的管道:

numbersFrom(2) -> filterPrimes(2) -> filterPrimes(3) -> filterPrimes(5) -> filterPrimes(7) ... 

测试无穷质数序列:

    fun producePrimesSequences() = runBlocking {var producerJob = numbersProducer(context, 2)while (true) {val prime = producerJob.receive()print("${prime} \t")producerJob = filterPrimes(context, producerJob, prime)}}

运行上面的代码,我们将会看到控制台一直在无限打印出质数序列:

螢幕快照 2017-07-14 01.41.41.png

9.11.3 通道缓冲区

我们可以给通道设置一个缓冲区:

fun main(args: Array<String>) = runBlocking<Unit> {val channel = Channel<Int>(4) // 创建一个缓冲区容量为4的通道launch(context) {repeat(10) {println("Sending $it")channel.send(it) // 当缓冲区已满的时候, send将会挂起}}delay(1000)
}

输出:

Sending 0
Sending 1
Sending 2
Sending 3
Sending 4

9.12 构建无穷惰性序列

我们可以使用 buildSequence 序列生成器 ,构建一个无穷惰性序列。

    val fibonacci = buildSequence {yield(1L)var current = 1Lvar next = 1Lwhile (true) {yield(next)val tmp = current + nextcurrent = nextnext = tmp}}

我们通过buildSequence创建一个协程,生成一个惰性的无穷斐波那契数列。该协程通过调用 yield() 函数来产生连续的斐波纳契数。

我们可以从该序列中取出任何有限的数字列表,例如

println(fibonacci.take(16).toList())

的结果是:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

9.13 协程与线程比较

直接先说区别,协程是编译器级的,而线程是操作系统级的。

协程通常是由编译器来实现的机制。线程看起来也在语言层次,但是内在原理却是操作系统先有这个东西,然后通过一定的API暴露给用户使用,两者在这里有不同。

协程就是用户空间下的线程。用协程来做的东西,用线程或进程通常也是一样可以做的,但往往多了许多加锁和通信的操作。

线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。

协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。

线程是协程的资源。协程通过Interceptor来间接使用线程这个资源。

9.14 协程的好处

与多线程、多进程等并发模型不同,协程依靠user-space调度,而线程、进程则是依靠kernel来进行调度。线程、进程间切换都需要从用户态进入内核态,而协程的切换完全是在用户态完成,且不像线程进行抢占式调度,协程是非抢占式的调度。

通常多个运行在同一调度器中的协程运行在一个线程内,这也消除掉了多线程同步等带来的编程复杂性。同一时刻同一调度器中的协程只有一个会处于运行状态。

我们使用协程,程序只在用户空间内切换上下文,不再陷入内核来做线程切换,这样可以避免大量的用户空间和内核空间之间的数据拷贝,降低了CPU的消耗,从而大大减缓高并发场景时CPU瓶颈的窘境。

另外,使用协程,我们不再需要像异步编程时写那么一堆callback函数,代码结构不再支离破碎,整个代码逻辑上看上去和同步代码没什么区别,简单,易理解,优雅。

我们使用协程,我们可以很简单地实现一个可以随时中断随时恢复的函数。

一些 API 启动长时间运行的操作(例如网络 IO、文件 IO、CPU 或 GPU 密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。

协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

9.14.1 阻塞 vs 挂起

协程可以被挂起而无需阻塞线程。而线程阻塞的代价通常是昂贵的,尤其在高负载时,阻塞其中一个会导致一些重要的任务被延迟。

另外,协程挂起几乎是无代价的。不需要上下文切换或者 OS 的任何其他干预。

最重要的是,挂起可以在很大程度上由用户来控制,我们可以决定挂起时做些,并根据需求优化、记日志、拦截处理等。

9.15 协程的内部机制

9.15.1 基本原理

协程完全通过编译技术实现(不需要来自 VM 或 OS 端的支持),挂起机制是通过状态机来实现,其中的状态对应于挂起调用。

在挂起时,对应的协程状态与局部变量等一起被存储在编译器生成的类的字段中。在恢复该协程时,恢复局部变量并且状态机从挂起点接着后面的状态往后执行。

挂起的协程,是作为Continuation对象来存储和传递,Continuation中持有协程挂起状态与局部变量。

关于协程工作原理的更多细节可以在这个设计文档中找到:https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md。

9.15.2 标准 API

协程有三个主要组成部分:

  • 语言支持(即如上所述的挂起功能),
  • Kotlin 标准库中的底层核心 API,
  • 可以直接在用户代码中使用的高级 API。
  • 底层 API:kotlin.coroutines

底层 API 相对较小,并且除了创建更高级的库之外,不应该使用它。 它由两个主要包组成:

kotlin.coroutines.experimental 带有主要类型与下述原语:

createCoroutine()
startCoroutine()
suspendCoroutine()

kotlin.coroutines.experimental.intrinsics 带有甚至更底层的内在函数如 :

suspendCoroutineOrReturn

大多数基于协程的应用程序级API都作为单独的库发布:kotlinx.coroutines。这个库主要包括下面几大模块:

  • 使用 kotlinx-coroutines-core 的平台无关异步编程
  • 基于 JDK 8 中的 CompletableFuture 的 API:kotlinx-coroutines-jdk8
  • 基于 JDK 7 及更高版本 API 的非阻塞 IO(NIO):kotlinx-coroutines-nio
  • 支持 Swing (kotlinx-coroutines-swing) 和 JavaFx (kotlinx-coroutines-javafx)
  • 支持 RxJava:kotlinx-coroutines-rx

这些库既作为使通用任务易用的便利的 API,也作为如何构建基于协程的库的端到端示例。关于这些 API 用法的更多细节可以参考相关文档。

本章小结

本章我通过大量实例学习了协程的用法;同时了解了作为轻量级线程的协程是怎样简化的我们的多线程并发编程的。我们看到协程通过挂起机制实现非阻塞的特性大大提升了我们并发性能。

最后,我们还简单介绍了协程的实现的原理以及标准API库。Kotlin的协程的实现大量地调用了Java中的多线程API。所以在Kotlin中,我们仍然完全可以使用Java中的多线程编程。

下一章我们来一起学习Kotlin与Java代码之间的互相调用。

本章示例代码工程:

https://github.com/EasyKotlin/chapter9_coroutines


Kotlin开发者社区

专注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函数式编程、编程思想、"高可用,高性能,高实时"大型分布式系统架构设计主题。

High availability, high performance, high real-time large-scale distributed system architecture design

分布式框架:Zookeeper、分布式中间件框架等
分布式存储:GridFS、FastDFS、TFS、MemCache、redis等
分布式数据库:Cobar、tddl、Amoeba、Mycat
云计算、大数据、AI算法
虚拟化、云原生技术
分布式计算框架:MapReduce、Hadoop、Storm、Flink等
分布式通信机制:Dubbo、RPC调用、共享远程数据、消息队列等
消息队列MQ:Kafka、MetaQ,RocketMQ
怎样打造高可用系统:基于硬件、软件中间件、系统架构等一些典型方案的实现:HAProxy、基于Corosync+Pacemaker的高可用集群套件中间件系统
Mycat架构分布式演进
大数据Join背后的难题:数据、网络、内存和计算能力的矛盾和调和
Java分布式系统中的高性能难题:AIO,NIO,Netty还是自己开发框架?
高性能事件派发机制:线程池模型、Disruptor模型等等。。。

合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。不积跬步,无以至千里;不积小流,无以成江河。

Kotlin 简介

Kotlin是一门非研究性的语言,它是一门非常务实的工业级编程语言,它的使命就是帮助程序员们解决实际工程实践中的问题。使用Kotlin 让 Java程序员们的生活变得更好,Java中的那些空指针错误,浪费时间的冗长的样板代码,啰嗦的语法限制等等,在Kotlin中统统消失。Kotlin 简单务实,语法简洁而强大,安全且表达力强,极富生产力。

Java诞生于1995年,至今已有23年历史。当前最新版本是 Java 9。在 JVM 生态不断发展繁荣的过程中,也诞生了Scala、Groovy、Clojure 等兄弟语言。

Kotlin 也正是 JVM 家族中的优秀一员。Kotlin是一种现代语言(版本1.0于2016年2月发布)。它最初的目的是像Scala那样,优化Java语言的缺陷,提供更加简单实用的编程语言特性,并且解决了性能上的问题,比如编译时间。 JetBrains在这些方面做得非常出色。

Kotlin语言的特性

用 Java 开发多年以后,能够尝试一些新的东西真是太棒了。如果您是 Java 开发人员,使用 Kotlin 将会非常自然流畅。如果你是一个Swift开发者,你将会感到似曾相识,比如可空性(Nullability)。 Kotlin语言的特性有:

1.简洁

大幅减少样板代码量。

2.与Java的100%互操作性

Kotlin可以直接与Java类交互,反之亦然。这个特性使得我们可以直接重用我们的代码库,并将其迁移到 Kotlin中。由于Java的互操作性几乎无处不在。我们可以直接访问平台API以及现有的代码库,同时仍然享受和使用 Kotlin 的所有强大的现代语言功能。

3.扩展函数

Kotlin 类似于 C# 和 Gosu, 它提供了为现有类提供新功能扩展的能力,而不必从该类继承或使用任何类型的设计模式 (如装饰器模式)。

4.函数式编程

Kotlin 语言一等支持函数式编程,就像Scala一样。具备高阶函数、Lambda 表达式等函数式基本特性。

5.默认和命名参数

在Kotlin中,您可以为函数中的参数设置一个默认值,并给每个参数一个名称。这有助于编写易读的代码。

6.强大的开发工具支持

而由于是JetBrains出品,我们拥有很棒的IDE支持。虽然Java到Kotlin的自动转换并不是100% OK 的,但它确实是一个非常好的工具。使用 IDEA 的工具转换Java代码为 Kotlin 代码时,可以轻松地重用60%-70%的结果代码,而且修改成本很小。

Kotlin 除了简洁强大的语法特性外,还有实用性非常强的API以及围绕它构建的生态系统。例如:集合类 API、IO 扩展类、反射API 等。同时 Kotlin 社区也提供了丰富的文档和大量的学习资料,还有在线REPL。

A modern programming language that makes developers happier. Open source forever

图来自《Kotlin从入门到进阶实战》 (陈光剑,清华大学出版社)

图来自《Kotlin从入门到进阶实战》 (陈光剑,清华大学出版社)

https://kotlinlang.org/

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 高并发下如何减少对MySQL集群的压力

    高并发下减少对MySQL集群的压力前言一、是否有高并发流量&#xff1f;二、慢SQL优化1.排查慢SQL2.导致慢SQL的原因3.解决方案总结前言 在高并发下&#xff0c;大量的服务调用中&#xff0c;需要经常对数据库频繁的做操作&#xff0c;但是此时也会带来cpu飙高的问题&#xff0c…...

    2024/4/24 10:31:16
  2. 《Kotin 极简教程》第10章 Kotlin与Java互操作

    第10章 Kotlin与Java互操作 Kotlin is 100% interoperable with Java™ and Android™ 在前面的章节中&#xff0c;我们已经学习了Kotlin的基础语法、类型系统、泛型与集合类、面向对象与函数式编程等主题&#xff0c;在上一章中我们还看到了Kotlin提供的轻量级并发编程模型&am…...

    2024/4/25 6:56:34
  3. jmeter压测oracle

    下载oracle驱动 <dependency><groupId>com.oracle.jdbc</groupId><artifactId>ojdbc6</artifactId><version>${ojdbc.version}</version> </dependency> 拷贝驱动 把驱动放到jmeter的lib目录下 添加 设置jdbc连接 添加jdbc…...

    2024/4/22 14:01:26
  4. CSS样式更改——用户界面和指针类型

    ###前言 上篇文章主要讲述了CSS样式更改中的多列、元素是否可见、图片透明度基础知识&#xff0c;这篇文章我们来介绍下CSS样式更改中用户界面和指针类型基础用法。 ####1.用户界面 UserGui 1).重设元素大小 resize div { resize:both } none 不调整 both …...

    2024/4/7 10:38:00
  5. time-formater 时间格式化插件

    time-formater 不是 time-format[t]er English 在javascript中显示日期。 使用方法 下载 npm i -S time-formaterlet rawDate time().format(YYYY-MM-DD HH:mm:ss) // 当前时间console.log(rawDate) // 2020-12-28 15:19:34解析 当前时间 let now time() 如果参数为空则获…...

    2024/4/22 4:04:37
  6. OceanBase架构初探

    本文源自&#xff1a;https://blog.csdn.net/jiankunking/article/details/84020030 1、设计思路 OceanBase的目标是支持数百TB的数据量以及数十万TPS、数百万QPS的访问量&#xff0c;无论是数据量还是访问量&#xff0c;即使采用非常昂贵的小型机甚至是大型机&#xff0c;单…...

    2024/4/7 10:37:57
  7. 风控-数据分析

    数据总体了解&#xff1a; 读取数据集并了解数据集大小&#xff0c;原始特征维度&#xff1b; 通过info熟悉数据类型&#xff1b; 粗略查看数据集中各特征基本统计量&#xff1b; 缺失值和唯一值&#xff1a; 查看数据缺失值情况 查看唯一值特征情况 深入数据-查看数据类…...

    2024/4/23 4:56:36
  8. 如何求解大多数动态规划(DP)问题

    From-good-to-great.-How-to-approach-most-of-DP-problems. 解决思路&#xff1a; 查找递归关系递归&#xff08;自上而下&#xff09;递归记忆数组&#xff08;自上而下&#xff09;迭代记忆数组&#xff08;自下而上&#xff09;迭代N个变量&#xff08;自下而上&#xff…...

    2024/4/25 1:34:40
  9. 独立站Shopify VS 亚马逊FBA !跨境电商创业哪个平台更适合个人创业?

    做跨境电商您选择好做那个平台了吗&#xff1f;不确定如何选择&#xff1f;也许您一直在评估不同的平台&#xff0c;但没有确定下来&#xff01;也许您已经将其范围缩小到这两个选项&#xff0c;或者您仍在寻找更多选择。 无论哪种方式&#xff0c;让我们来看一下为什么Shopif…...

    2024/4/7 10:37:59
  10. 数据库:MySQL查询一个表的所有字段

    SELECTCOLUMN_NAME FROMINFORMATION_SCHEMA. COLUMNS WHEREtable_name 查询的数据表名 AND table_schema 数据库名...

    2024/4/22 17:31:28
  11. LeetCode 155. 最小栈

    问题描述&#xff1a; 代码&#xff1a; class MinStack { public:stack<int> data;stack<int> min;MinStack() {}void push(int x) {data.push(x);if (min.empty() || min.top() > x) //总是把最小的元素压到min栈里面{min.push(x);}else{min.push(min.top())…...

    2024/4/22 23:52:33
  12. 获取exe路径

    #include <string> #include <iostream> #include <Windows.h>using namespace std;string GetExePath(void) {char szFilePath[MAX_PATH 1] { 0 };GetModuleFileNameA(NULL, szFilePath, MAX_PATH);(strrchr(szFilePath, \\))[0] 0; // 删除文件名&#…...

    2024/4/7 11:24:08
  13. servlet+mysql+filter+jsp项目:破烂音乐

    上个学期完成了Java web课程学习后&#xff0c;按照课程要求&#xff0c;要做一个servletmysqlfilterjsp的项目&#xff0c;由于时间有限&#xff0c;在上网逛了一圈后就在52论坛找到一个坛友分享的音乐播放器项目&#xff0c;这里非常感谢&#xff1a; https://www.52pojie.cn…...

    2024/4/7 11:24:07
  14. Ubuntu18.04更换国内源

    Ubuntu18.04更换国内源 这里讲解如何ubuntu系统如何更换阿里云源&#xff0c;作为科研的起步&#xff0c;跟着我&#xff0c;一步一步进入到机器学习的世界中吧&#xff01; 1.打开源文件 sudo vi /etc/apt/sources.list dd(长按) i 鼠标右键-粘贴你复制的源 esc :wq2.将原有…...

    2024/4/7 11:24:06
  15. 知道车架号,vin想查询车辆维修和出险记录,以上 JavaScript 代码可以通过 HTML 代码来调用

    function validateForm() {var x document.forms["myForm"]["fname"].value;if (x null || x "") {alert("需要输入名字。");return false;} } 知道车架号&#xff0c;vin想查询车辆维修和出险记录。 现在使用手机微信&#xff0…...

    2024/4/12 0:00:07
  16. 【LeetCode每日一题】47. 全排列 II

    【LeetCode每日一题】47. 全排列 II 47. 全排列 II 题目来源link 算法思想&#xff1a;回溯法&#xff0c;递归&#xff1b; 给定数组{1,1,2} 图片链接link PS&#xff1a;关键在于条件判断&#xff0c;用于回溯中剪枝&#xff0c;去重 // 这里理解used[i - 1]非常重要 …...

    2024/4/21 21:21:40
  17. 《Kotlin极简教程》第1章 Kotlin简介

    第1章 Kotlin简介 1.1 kotlin简史 1.1.1 Kotlin概述 科特林岛&#xff08;Котлин&#xff09;是一座俄罗斯的岛屿&#xff0c;位于圣彼得堡以西约30公里处&#xff0c;形状狭长&#xff0c;东西长度约14公里&#xff0c;南北宽度约2公里&#xff0c;面积有16平方公里&a…...

    2024/4/23 2:24:36
  18. 《Linux性能优化实战》笔记(八)—— 内存是怎么工作的

    一、 内存映射 我们通常所说的内存容量&#xff0c;指的是物理内存。物理内存也称为主存&#xff0c;大多数计算机用的主存都是动态随机访问内存&#xff08;DRAM&#xff09;。只有内核才可以直接访问物理内存。那么&#xff0c;进程要访问内存时&#xff0c;该怎么办呢&…...

    2024/4/20 19:16:37
  19. 5G网络的远程手术示教演示

    5G网络的远程手术示教演示 远程手术示教&#xff0c;大家并不陌生&#xff0c;在很多医院得到了普遍的应用。可是同时也受到带宽和信号的限制&#xff0c;无法真正达到身临其境的目的。而现在5G网络的出现&#xff0c;手术示教系统才能真正能够发挥其作用。 下面&#xff0c;…...

    2024/4/7 11:24:01
  20. 展讯8541E平台NFC PM1810驱动调试

    一&#xff0c;非接芯片PM1810介绍 1. PM1810管脚排列图&#xff1a; 2. PM1810部分接线图&#xff1a; 3. PM1810发射、接收电路&#xff1a; 二&#xff0c;驱动代码部分的修改&#xff1a; kernel4.4/arch/arm64/boot/dts/sprd/sl8541e-1h10-native.dts &spi0 {/*use…...

    2024/4/20 23:17:38

最新文章

  1. SpringBoot Aop使用篇

    Getting Started SpringBoot AOP的实践 AOP相关的概念&#xff1a; Aspect&#xff08;切面&#xff09;&#xff1a; Aspect 声明类似于 Java 中的类声明&#xff0c;在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。就是抽离出来的逻辑类&#xff0c;比如日志、权限…...

    2024/4/26 13:05:20
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. ubuntu添加固定路由

    方法&#xff1a; 我的解决方法 添加路由 sudo ip route add 10.xxx.xxx.0/25 via 1.xxx.xxx.xxx&#xff08;我的是虚拟机&#xff09;dev ens65 proto static metric122 删除路由 sudo ip route delete 10.xxx.xxx.0/25 gpt答案 添加路由 要在Ubuntu上添加路由&#xff0c;您…...

    2024/4/26 10:38:32
  4. 2024 年高效开发的 React 生态系统

    要使用 React 制作应用程序&#xff0c;需要熟悉正确的库来添加您需要的功能。例如&#xff0c;要添加某个功能&#xff08;例如身份验证或样式&#xff09;&#xff0c;您需要找到一个好的第三方库来处理它。 在这份综合指南中&#xff0c;我将向您展示我建议您在 2024 年使用…...

    2024/4/25 21:42:20
  5. 是否有替代U盘,可安全交换的医院文件摆渡方案?

    医院内部网络存储着大量的敏感医疗数据&#xff0c;包括患者的个人信息、病历记录、诊断结果等。网络隔离可以有效防止未经授权的访问和数据泄露&#xff0c;确保这些敏感信息的安全。随着法律法规的不断完善&#xff0c;如《网络安全法》、《个人信息保护法》等&#xff0c;医…...

    2024/4/23 23:05:24
  6. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/4/26 1:36:40
  7. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/4/25 21:14:51
  8. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/4/26 8:22:40
  9. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/4/26 11:10:01
  10. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/4/25 16:50:01
  11. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/4/25 13:02:58
  12. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/4/26 0:25:04
  13. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/4/26 6:06:14
  14. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/4/25 17:43:17
  15. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/4/25 17:43:00
  16. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/4/25 13:00:31
  17. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/4/25 17:42:40
  18. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/4/26 9:43:47
  19. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/4/26 9:43:47
  20. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/4/25 13:40:45
  21. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/4/25 13:01:30
  22. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/4/25 15:31:26
  23. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/4/25 17:31:15
  24. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/4/25 13:22:53
  25. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/4/26 9:43:45
  26. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  27. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  28. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  29. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  30. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  31. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  32. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  33. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  34. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  35. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  36. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  37. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  38. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  39. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  40. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  41. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  42. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  43. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  44. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  45. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57