美文网首页Kotlin-Coroutines
深入学习Kotlin协程(一)-协程是什么?

深入学习Kotlin协程(一)-协程是什么?

作者: kevinsEegets | 来源:发表于2020-12-14 18:26 被阅读0次

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协程序二-协程的作用。

相关文章

网友评论

    本文标题:深入学习Kotlin协程(一)-协程是什么?

    本文链接:https://www.haomeiwen.com/subject/pbqbgktx.html