Kotlin协程是什么?
协程并不是Kotlin提出的,其他的一些编程语言在早期就已经实现了协程,比如: Go、Python、Lua等语言。
当我们查看Kotlin官方文档时,可以看到一句很醒目的话
本质上,协程是轻量级的线程。
我们把这句话拆分一下通过例子理解会透彻很多,我们尝试将上述这句话拆分为两个单词:轻量级
和线程
线程
首先我们都知道什么是线程,并且怎么创建一个线程,这节课我们不了解什么是线程,我们看看Kotlin的协程为什么被称为线程
我们沿用官方demo看看
fun coroutineTest() {
repeat(100_000) {
GlobalScope.launch {
delay(10000L)
println("World i=$it threadName=${Thread.currentThread().name}, threadId=${Thread.currentThread().id}")
}
println("World i=$it")
}
Thread.sleep(10000L)
}
fun main() {
coroutineTest()
}
打印一下部分日志
World i=97490 threadName=DefaultDispatcher-worker-1, threadId=11
World i=97491 threadName=DefaultDispatcher-worker-1, threadId=11
...
World i=97492 threadName=DefaultDispatcher-worker-9, threadId=19
...
World i=96774 threadName=DefaultDispatcher-worker-10, threadId=20
World i=96756 threadName=DefaultDispatcher-worker-12, threadId=22
World i=97496 threadName=DefaultDispatcher-worker-12, threadId=22
World i=97531 threadName=DefaultDispatcher-worker-12, threadId=2
...
World i=97621 threadName=DefaultDispatcher-worker-11, threadId=21
World i=97656 threadName=DefaultDispatcher-worker-11, threadId=21
...
World i=96749 threadName=DefaultDispatcher-worker-3, threadId=13
World i=97675 threadName=DefaultDispatcher-worker-3, threadId=13
...
World i=96724 threadName=DefaultDispatcher-worker-2, threadId=12
World i=96731 threadName=DefaultDispatcher-worker-2, threadId=12
...
World i=96721 threadName=DefaultDispatcher-worker-4, threadId=14
World i=96733 threadName=DefaultDispatcher-worker-4, threadId=14
Process finished with exit code 0
因为我们有十万条日志,所以上述日志适当性的筛选了一些
通过日志可以看出,使用Kotlin协程创建的Worker
线程是可以复用的,这也就解释了Kotlin协程是线程
的意义,本质上Kotlin协程就是创建了一个可以复用的线程,注意是可以复用的线程.那如果创建一个不可以复用的线程会出现什么情况?
这就是Kotlin官方提出的一个比较,官方文档如下
如果你首先将
GlobalScope.launch
替换为thread
,编译器会报以下错误:Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
这是因为 delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。
我们用Thread
替换GlobalScope.launch
创建一个十万个线程,看看到底会发生什么?
fun threadTest() {
repeat(100_0000) {
Thread {
sleep(10000L)
println("World it = $it threadName=${Thread.currentThread()}, threadId=${Thread.currentThread().id}")
}.start()
println("threadId it = $it")
}
Thread.sleep(10000L)
}
fun main() {
threadTest()
}
我们还是打印一下输出
...
threadId it = 6921
threadId it = 6922
threadId it = 6923
threadId it = 6924
threadId it = 6925
threadId it = 6926
threadId it = 6927
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at com.bennyhuo.kotlin.coroutinebasics.kevin.CoroutineTestKt.threadTest(CoroutineTest.kt:49)
at com.bennyhuo.kotlin.coroutinebasics.kevin.CoroutineTestKt.main(CoroutineTest.kt:68)
at com.bennyhuo.kotlin.coroutinebasics.kevin.CoroutineTestKt.main(CoroutineTest.kt)
报错日志也一目了然,当创建第6928个线程时报错了,错误的提示是我们不能创建新的线程了,也就是线程数达到顶峰了,这是为什么呢?
主要原因就是Thread创建的是单线程,不可复用的线程,内存是有限定的,这个值不是固定的,跟手机性能等都有关系(猜想,不对的大佬可以纠正)。
另外一个重要的原因跟协程的delay()
有关,我们可以看看kotlin协程官网说法
这是因为 delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。
协程实现中我们使用了 非阻塞的 delay(……)
而线程实现中使用了 阻塞的 Thread.sleep(……)
。这也是thread创建到第6928个线程后报错的部分原因。
现在,我们知道了Kotlin为什么被称为线程,我们再来看看为什么被称作轻量级线程
轻量级
轻量级的字面意思是:小巧,方便,简单,那根据字面意思,我们是不是可以这么理解:
协程是一种更简单、方便、轻巧的线程呢?
我们还是用常用的一个网络请求来说明一下
我们用okhttp+retrofit实现一个简单的网络请求,接口如下:
interface GitHubApi {
@GET("users/{login}")
fun getUserCallback(@Path("login") login: String): Call<User>
@GET("users/{login}")
suspend fun getUserSuspend(@Path("login") login: String): User
}
data class User(val id: String, val name: String, val url: String)
Retrofit初始化如下:
val gitHubServiceApi by lazy {
val retrofit = retrofit2.Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(GitHubServiceApi::class.java)
}
我们请求网络时:
gitHubServiceApi.getUserCallback("bennyhuo").enqueue(object : Callback<User> {
override fun onFailure(call: Call<User>, t: Throwable) { //成功
handler.post { showError(t) }
}
override fun onResponse(call: Call<User>, response: Response<User>) { //失败
handler.post { response.body()?.let(::showUser) ?: showError(NullPointerException())}
}
})
以上代码就是我们经常用来实现网络请求的步骤,好点的话会用rxjava观察者模式实现
我们现在用协程改造一下请求网络部分:
GlobalScope.launch(Dispatchers.Main) {
try {
showUser(gitHubServiceApi.getUserSuspend("bennyhuo").await()) //成功
} catch (e: Exception) {
showError(e) //失败
}
}
这里需要注意一下:要使用协程我们需要升级Retrofit版本大于2.6.0,并且在GitHubApi
中增加suspend
关键字
如上代码,我们先不管是什么是Globalcope、suspend以及Dispatchers,我们只看如上代码,是不是比普通方式实现的简单,从代码看出,我们不再用回调的形式实现,而是直接在try内部执行了我们的showUser
操作,这种实现是不是比用原始的回调或rxjava的观察者模式实现要简单的多
小结
我们用两段代码搞清楚了kotlin协程的官方定义:协程是轻量级线程,简单来说Kotlin协程就是Kotlin给我们提供的一套:简单、方便、轻巧的线程库,能帮助我们快速的实现网络请求,简化代码逻辑。
既然协程这么好,那他到底有哪些作用呢?可以查看第二篇文章:深入学习Kotlin协程序二-协程的作用。