kotlin入门潜修之协程—基本概念

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

协程

从本篇文章开始,将会开启kotlin的另一个世界——协程,这是kotlin提供的异步处理机制。

提到异步,自然而然想起的就是多线程、多进程,这是绕不开的话题,但是本系列文章将暂时不阐述他们的区别,而是先将协程相关的知识阐述完毕后,在来做一个整体的对比以及剖析协程背后的原理。

本篇文章将会演示协程的基本使用方法。

Hello World!

本小节先来看一个协程的“hello world”。

首先,kotlin协程相关的接口是位于 kotlinx.coroutines包下的,因此,我们在使用协程的时候,要首先导入该包。但是该包并不位于kotlin标准库中(kotlin竟然没有提供标准库的协程支持),而是作为独立的库发布的,因此我们需要先引入协程相关的依赖。

在这里我们采用的是maven引入的方式,如下所示:

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

需要注意,kotlin的版本一定要在版本1.3以上才可以使用,因为1.2版本中的协程还是实验性质的,所以推荐使用最新的kotlin版本(对于idea可以升级kotlin插件,如果无法升级则需要升级idea至最新版本),这样才能用到相对稳定的协程相关的接口方法。

如果是使用gradle作为构建工具,则可以像下面一样引入依赖:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
}

这两种基本已经满足我们的使用需求了(maven常用于java工程,gradle则常用于android工程),其他构建方式就不再阐述。

下面来看下kotlin协程之“hello world”示例,如下所示:

fun main(args: Array<String>) {
    GlobalScope.launch {
        delay(1000L)
        println("world!")
    }

    println("hello ")
    Thread.sleep(2000L)
}

上面的代码执行完成后,打印结果如下所示:

hello 
world!

GlobalScope.launch表示启动一个后台协程,该协程的任务是延时1000ms后打印“world!”字符。当kotlin启动过该协程后,会继续执行其后面的代码,所以我们看到先打印出了“hello ”字符串。需要注意,我们在打印hello字符串语句之后又调用了Thread.sleep方法,并延时等待了2000ms(理论上只要稍微大于GlobalScope.launch中的延时时间即可),这么做的原因是保持jvm继续存活,否则程序执行完后(即main方法线程执行完后)就会被终止,而不会执行GlobalScope.launch中延时后的打印语句(后面会有新的机制可以不必这么做),这点和多线程不一样。此外,采用GlobalScope启动的协程(它是top-level级别的协程),其生命周期将会伴随整个应用程序的生命周期。

再来看一下delay方法,该方法的声明如下所示:

suspend fun delay(timeMillis: Long):Unit

该方法的意思是,延迟协程的执行,延迟时间由timeMillis决定,表示延迟的毫秒数。delay方法并不会阻塞线程,而仅仅会挂起当前协程,并在其指定延迟的时间到达时唤醒当前协程。

由上面代码可知,delay方法使用了suspend关键字进行修饰,故名思议,suspend修饰的方法表示该方法是个可挂起方法,只能用于协程中或者被suspend方法调用。

上面示例中,实际上用到了java中的Thread线程机制(用于阻塞main线程),其实kotlin本身也提供了一种机制来阻塞线程,如下所示:

fun main(args: Array<String>) {
    GlobalScope.launch {
        delay(1000L)
        println("world!")
    }
    println("hello ")
    runBlocking {
        delay(2000L)
    }
}

上面代码执行结果同样会打印“hello world!”,runBlocking的作用就是用于阻塞main线程,以保持jvm的存活(否则程序运行结束后会被回收)。注意,前面说过delay方法并不阻塞线程,所以这里不要误以为是delay方法在阻塞main线程,其背后的机制实际上是,main线程会等待runBlocking中的代码执行完成!

我们也可以结合runBlocking来采用多协程的方式完成上述功能,如下所示:

fun main(args: Array<String>) = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println("world!")
    }
    println("hello ")
    delay(2000L)
}

打印结果同上。这里需要注意,我们使用runBlocking方法启动了main协程,然后又在main协程中启动了一个新的后台协程(即GlobalScope.launch ),因为main协程中的delay方法运行在runBlocking方法中,所以这种写法,依然能起到“阻塞main线程”的目的(main线程需要等待runBlocking运行完成)。

runBlocking<Unit>中的Unit表示main方法的返回值,这里只不过是进行了显示指定,其默认返回值其实就是Unit。

使用delay方法可以起到延迟等待的作用,但是这显然不是一个好的实现方式,毕竟程序执行的时间有些时候并不是确定的。如果我们想实现健全的等待机制,可以利用kotlin为我们提供的job,如下所示:

fun main(args: Array<String>) = runBlocking {
    val job = GlobalScope.launch {
        delay(1000L)
        println("world!")
    }
    println("hello ")
    job.join()//这里会等待协程job执行完成
}

上面job的类型是kotlin为我们提供的接口Job,表示一个任务。后面文章会进行阐述。

结构化并发

在kotlin中,我们使用GlobalScope.launch启动的协程是top-level级别,虽然协程是轻量级的,但是依然会消耗资源。比如我们启动了很多协程,在运行的时候发生了未知错误,如果没有一个健全的机制回收这些资源,那么kotlin是不会自动帮我们处理的,针对这种情况,我们可以使用结构化并发。即像使用线程一样,在需要的时候创建,而不是创建top-level级别的协程,如下所示:

fun main(args: Array<String>) = runBlocking {
    launch {
        delay(500L)
        println("world!")
    }
    println("hello ")
}

上面代码同样会打印'hello world',与前面不同的是,我们没有再让main协程等待一定的时间,也没有使用join机制。这是因为,launch创建的协程,其作用域属于runBlocking(参见下节作用域构造器),此时,runBlocking会等待在作用域内启动的所有协程的执行,直到他们全部完成才会结束。

对于上面的代码,我们还可以结合前面提到的suspend方法做进一步整理,那就是将launch中的语句抽象出来,作为一个单独的方法,如下所示:

fun main(args: Array<String>) = runBlocking {
    launch {
        printWorld()
    }
    println("hello ")
}
//这里必须定义为suspend方法,否则无法使用同时suspend的delay方法。
suspend fun printWorld() {
    delay(500L)
    println("world!")
}

作用域构造器(Scope builder)

上面提到的协程作用域,实际上就是由不同的作用域构造器决定的,除了使用系统提供的默认作用域构造器,我们同样可以自己定义作用域构造器。kotlin为我们提供了创建作用域构造器的方法:coroutineScope。使用该方法会创建一个新的协程作用域,coroutineScope和runBlocking很相似,他们都会等待其作用域内的所有协程执行完成后才会结束,但是coroutineScope不会像runBlocking那样阻塞线程。来看个例子:

fun main(args: Array<String>) = runBlocking {
    launch {//launch属于runBlocking作用域
        delay(200L)
        println("Task from runBlocking")
    }
   //创建了一个新的协程作用域,属于runBlocking作用域
    coroutineScope {
        launch {//属于coroutineScope作用域
            delay(300L)
            println("Task from nested launch in  coroutine scope")
        }

        delay(100L)
        println("Task from coroutine scope")
    }
   //属于runBlocking作用域,该打印语句永远会在coroutineScope
//返回之后执行,但是如果和开始处的launch方法中的打印时机进行对比
//则取决于launch的delay时间
    println("coroutine scope is over")
}

我们先分析下上面的代码:

首先,我们使用runBlocking创建了一个main协程,在其代码块中,我们使用launch启动了一个新协程,该协程作用域属于main协程,launch启动的协程执行时机将会有其内部的delay时间决定。

接着,我们使用coroutineScope创建了一个新的协程作用域,该协程会等待其作用域内的所有协程执行完成后才会结束。这意味着,在这个示例中,只有coroutineScope内部的所有协程执行完成之后,coroutineScope才会返回,也就是说 println("coroutine scope is over")
这条语句永远都会在coroutineScope返回之后才执行。

最后,再结合不同的delay时间就不难推测出打印结果,如下所示:

Task from coroutine scope
Task from runBlocking
Task from nested launch in  coroutine scope
coroutine scope is over

Global 协程

前面已经多次提到了Global协程,这里在明确一下它的概念。

在kotlin中,Global协程相当于后台线程的概念,即Gloabal协程并不会保证进程的存活,应用程序结束了它就结束了,来看个例子:

fun main(args: Array<String>) = runBlocking {
    GlobalScope.launch {
        repeat(100) { i ->
            println(i)
            delay(100L)
        }
    }

    delay(300L)
    println("end...")
}

上面代码执行过后打印如下:

0
1
2
3
end...

也就是说,程序什么时候结束,取决于runBlocking方法delay的时间,这也就是文章刚开始的时候,我们使用Global协程,为什么要等待一定时间的原因。当然,看到这里,我们显然有很多解决方法了(比如将GlobalScope.launch改为launch),这里只是更进一步的说明一下GlobalScope协程的机制。

至此,本篇文章阐述完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容