Kotlin:协程

kotlin&android.png

前言

恭喜小伙伴们开始学习Kotlin最富有特色的一篇知识,这也是我所输出的整个Kotlin中最后的一篇文章了,,我会尽自己最大的努力将这篇内容输出好
其实在大部分语言中其实没有协程这个概念的。
那么什么是协程呢,他其实和java的线程有一些类似,我们可以将它理解为一种轻量级的线程
java的线程是非常重量级的,它需要利用操作系统的调度才能实现不同线程中间的切换,从而大大的增加了并发编程的执行效率。
而且协程对于Kotlin来说一直是一个经典必谈的内容
查看以下代码

fun one() {
    a()
    b()
    c()
}
fun two() {
    d()
    e()
    f()
}

当我们在没有开启线程的情况下依次执行one和two方法,那么理论上结果一定是a()、b()、c(),执行完了之后会调用d()、e()、f()。而如果使用了协程在协程A中调用one,在协程B中调用two,这时候他们是运行在一个线程中的但是在执行one方法时随时都有可能被挂起而去执行two方法,而在执行two方法的时候随时都有可能被挂起去执行one方法,最终输出结果也就不确定了。
以上现象可以得出结论:协程允许我们在单线程的环境下模拟出多线程环境,代码执行的逻辑由编程语言控制,可操作性系统无关,这种特性使得高并发程序的运行效率得到极大提升,可以想象一下我们开启10万个线程是不能够想象的事情吧,但是我们却可以开始10万个协程~

上边我们已经了解了一些协程的基本概念,相信大家已经迫不及待想要学习了~
发车了兄弟们GO GO GO ~

一:协程的基本用法

 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"

第二个是在android项目中的用到的

1.1:开启一个协程的最简单方法如下

fun main() {
    GlobalScope.launch {
        println("这是协程作用域")
    }
    Thread.sleep(1000)
}

使用GlobalScope.lunch函数创建一个顶层协程作用域,这种协程当应用程序运行结束时也会跟着一起结束。你会发现我们在代码中如果没有加 Thread.sleep(1000)打印语句并不会打印也就是这个原因。

再次观察如下代码

fun main() {
    GlobalScope.launch {
        println("这是协程作用域")
        delay(1500)
        println("我使用delay挂起了函数")
    }
    Thread.sleep(1000)
}

delay函数的作用是让当前协程延时多久执行,它是一个非阻塞式的挂起函数,只作用在当前协程,不会影响其他协程,Thread.sleep则是阻塞线程
我们发现我们新增的打印并没打印,因为我们使用了delay函数,不打印是理所当然的效果。

1.2:runBlocking开启协程

经过上边的介绍我们知道了GlobalScope.Lunch函数在我们程序执行完毕之后也会强制关闭,那么有没有方法创建协程让程序在协程执行之后再结束呢?下边学习一下runBlocking{}

    runBlocking {
        println("这是协程作用域")
        delay(1500)
        println("我使用delay挂起了函数")
    }

我们运行之后会发现打印信息可以全部打印
runBlocking函数同样会创建一个协程的作用域,但是他的可以保证协程作用域内的所有代码和子协程没有执行完之前完全阻塞当前线程,需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境也许会出现一些性能问题

1.3:创建多个协程

上边我们学习两种开启协程的方式,但是我们好像并没有发现协程与普通线程的区别,因为我们当前的代码都是运行在同一个协程当中的,当我们一旦涉及高并发的时候协程的优势就能体现出来了。
那么怎么才能创建多个协程呢?请继续往下看

    runBlocking {
        launch {
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }

我们这里的的lunch函数和刚才的GlobalScope.launch函数不同,它必须在一个协程作用域中才能调用,其次他会在当前协程作用域下创建子协程,子协程的特点是如果外层的作用域结束了,该作用域下的所有子协程也会一同结束。

多协程并发运行结果.png

我们看到打印结果两个子线程中的日志是交替进行的,说明他确实像线程一样并发运行,然而这两个子协程实际却运行在同一个线程当中,这就是我们一开始所说的只由编程语言来决定如何在多个协程之间进行调度,过程不需要操作系统参与,所以这样我们的并发效率会出奇的高
我们使用如下代码做一个简单的测试

    val startTime = System.currentTimeMillis()
    runBlocking {
        repeat(100000) {
            launch {
                println(".")
            }
        }
    }
    println("${System.currentTimeMillis() - startTime}")

我们使用了repeat创建了一个十万个协程,执行结果如下


10万个协程并发运行结果.png

我们看到仅仅运行了455毫秒,可以看到协程的运行效率有多高,但是如果我们使用线程的话可能需要比较长的时间...

1.4:suspend关键字

当我们在launch函数中的逻辑越来越复杂的时候,我们可能需要将数据部分代码提取到一个单独的函数中,这时候就出现了一个问题:我们在lunch函数中是存在协程作用域的,提取出去不就没有作用域了吗????
为此kotlin 提供了一个suspend关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间是可以相互调用的

suspend fun printDo() {
    println()
    delay(1000)
}

1.5:coroutineScope关键字

请注意:suspend关键字只能将一个函数挂起,并不能提供协程作用域,比如如果你想在printDo中使用launch函数是不能调用到的。还好我们可以使用coroutineScope函数来解决,coroutineScope也是一个挂起函数,它的特点是继承外部的协程作用域并创建一个子协程 ,借助这个特性我们就可以给任何挂起的函数提供作用域了,另外coroutineScope关键字和runBlocking函数还有点类似,可以保证其作用域内的所有代码和子协程在全部执行完之前,外部协程一直被挂起

    runBlocking {
        coroutineScope {
            launch {
                for (i in 1..10) {
                    println("当前打印数据是${i}")
                }
            }
        }
        println("coroutineScope 执行结束")
    }
    println("runBlocking 执行结束")

可以看到上方代码,首先使用runBlock创建一个协程,然后再使用coroutineScope创建一个子协程,在coroutineScope中我们又创建了一个launch子协程,打印日志如下


image.png

由此可见,coroutineScope函数确实将外部协程(这里是launch)挂起了,并且只有在他的作用域内的代码全部执行完毕之后coroutineScope函数之后的代码才会得到运行

看上去coroutineScope和runBlocking函数的作用类似,但是coroutineScope只会阻塞当前协程,并不会影响其他协程和线程,因此不会造成任何性能问题,而runBlocking则会挂器外部线程,如果你恰巧在又在主线程中使用的话,可能会造成界面卡死。

1.4:更多的作用域构建器

之前我们学习了 GlobalScope.launch、runBlocking、launch、cornotineScope这几种作用域构建器
他们都可以用于创建一个新的协程作用域
GlobalScope.launch、runBlocking可以在任何地方调用;
cornotineScope可以在协程作用域或者挂起函数中调用;
launch函数只能在协程作用域中使用。

前边说过runBlocking在使用中会造成线程阻塞,所以只建议在测试环境下使用,而GlobalScope.launch作为顶层协程一般也不建议使用,这是为什么呢?
比如我们网络请求数据过程中,突然退出当前activity,此时应该取消这条网络请求,或者至少不应该回调,那么协程需要怎么取消呢?,不管是GlobalScope.launch函数还是launch函数,他们都返回一个Job对象,只需要调用job对象的cancel方法就可以

val job=Global.launch{
}
job.cancel()

但是如果我们每次创建的都是顶层协程,那么当前Activity关闭时,就需要逐个所有已创建协程的cancel方法...这样是不是会变得无法维护,因此GlobalScope.launch这种协程作用域构建器,在实际项目中也是不太常用,下面是一种实际项目中常用写法:

1.4.1:协程在实际项目中常用写法

    val job = Job()
    val scope = CoroutineScope(job)
    scope.launch {
    }
    job.cancel()

我们使用CoroutineScope创建的协程全部都在job作用域下,这样只需要调用一次cancel方法,就可以将所有作用域下的协程全部取消,从而减少协程管理成本,不知道小伙伴们get到的没。

1.4.2:async await

launch函数只能用于执行一段逻辑,却不能获取执行结果,我们可以使用async函数实现
async函数必须在协程的作用域下才能被调用,他会创建一个新的子协程并返回一个Deferred对象,如果我们想获取async函数代码块的执行结果,只需要调用Deferred对象的await方法即可

    runBlocking {
        val result = async {
            5 + 5
        }.await()
        println(result)
    }

另外在调用了async代码之后,代码块中的代码就会立刻开始执行,当调用到await方法时候,如果当前代码没有执行完毕,那么await会阻塞当前协程,直到可以获取执行结果。
我们使用如下代码进行测试

    runBlocking {
        val start = System.currentTimeMillis()
        val result1 = async {
            delay(1000)
            1 + 2
        }.await()
        val result2 = async {
            delay(1000)
            24 - 1
        }.await()
        println("返回结果是:${result1 + result2}")
        val end = System.currentTimeMillis()
        println("操作执行时间为:${end - start}")
    }
async串行.png

以上代码是一个执行2025毫秒,说明是一个串行关系,接下来我们更改一下代码,优化代码执行效率

  runBlocking {
        val start = System.currentTimeMillis()
        val result1 = async {
            delay(1000)
            1 + 2
        }
        val result2 = async {
            delay(1000)
            24 - 1
        }
        println("返回结果是:${result1.await() + result2.await()}")
        val end = System.currentTimeMillis()
        println("操作执行时间为:${end - start}")
    }
async并行.png

我们不在async中执行之后立即获取结果,而是在需要使用结果的时候再调用await这样就可以节省代码执行时间。因为我们让两个async同时执行了。

1.4.3:withContext函数

最后我们来学习一个比较特殊的作用域构建器withContext函数,withContext函数是一个挂起函数,大体可以将它理解成async函数的一种简化版写法,如果你在代码中实现了async await 那么编译器会提示你让你转化为withContext,实列如下:

    runBlocking {
        val result = withContext(Dispatchers.Default) {
            5 + 5
        }
        println(result)
    }

代码在调用到withContext函数之后会立即,执行代码块中的代码,同时将外部协程挂起,当代码块中的代码全部执行完毕之后,会将最后一行的执行结果为withContext函数的返回值返回,基本上跟
val result=async{5+5}.await()的写法相同,唯一不同的是在withContext需要我们指定一个线程参数,
下面我们讲一下这个线程参数
我们知道协程是一种轻量级的线程,因此很多传统编程情况下需要开启多线程执行的并发任务,现在我们只需要开启多个协程来执行就可以了,但这并不意味着我们就可以不需要开启线程了,比如说android中要求网络请求必须在子线程中执行,即使你开启了协程去执行网络请求,假如他是主线程中的协程,那么程序依然会报错。这个时候我们应该通过线程参数给协程指定一个具体的运行线程
线程参数主要有以下三种参数可选
1:Dispatchers.Default :表示会使用一种默认低并发的线程策略,比如你使用计算密集型任务的时候,开启过高的并发反而可能会影响任务的运行效率,此时就应该使用Dispatchers.Default
2:Dispatchers.Main :表示不会开启子线程,而是在Android主线程中执行代码,这个值只能在Android项目中执行
3:Dispatchers.IO:代表一种高并发线程策略,当你执行的代码大多数时间在等待或者阻塞中,比如执行网络请求,为了能更高的并发数量,此时就可以使用Dispatchers.IO

1.5:使用协程简化回调的写法

我们来看一下使用Kotlin实现一个网络回调的写法:

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
 override fun onFinish(response: String) {
 // 得到服务器返回的具体内容
 }
 override fun onError(e: Exception) {
 // 在这里对异常情况进行处理
 }
})

这种写法的弊端就是在有多少的地方发起网络请求,接下来我们可以使用kotlin进行的suspendCoroutine函数就能将传统回调机制大幅简化
suspendCoroutine函数必须在协程作用域或着挂起函数中调用,它接收一个lambda表达式,作用是将当前协程立即挂起然后在一个普通线程中执行lambda表达式中的代码。lambda表达式的参数列表上会传入一个coutinuation参数,调用它的resume方法或者resumeWithException可以让协程恢复

    suspend fun request(address: String): String {
        return suspendCoroutine { continuation ->
            HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
                override fun onFinish(response: String) {
                    continuation.resume(response)
                }
                override fun onError(e: Exception) {
                    continuation.resumeWithException(e)
                }
            })
        }
    }

以上代码在执行到suspendCoroutine函数的时候协程会被挂起,进行执行lambda中的代码,请求成功调用resume,失败调用resumeWithException,并恢复被挂起的协程,你可能会说这里不是使用了传统回调写法吗,代码怎么就变得更加简洁了呢,这是因为我们不管发起多少次请求,都不需要进行重复的回调了。
比如以下代码

    suspend fun getBaiduResponse() {
        try {
            val response = request("https://www.baidu.com/")
            // 对服务器响应的数据进行处理
        } catch (e: Exception) {
            // 对异常情况进行处理
        }
    }

事实上suspendCoroutine函数几乎可以简化任何回调的写法,比如之前使用retrfit来发起网络请求:

    val appService = ServiceCreator.create<AppService>()
    appService.getAppData().enqueue(object : Callback<List<App>> {
        override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
            // 得到服务器返回的数据
        }
        override fun onFailure(call: Call<List<App>>, t: Throwable) {
            // 在这里对异常情况进行处理
        }
    })

更改如下:

    suspend fun <T> Call<T>.await(): T {
        return suspendCoroutine { continuation ->
            enqueue(object : Callback<T> {
                override fun onResponse(call: Call<T>, response: Response<T>) {
                    val body = response.body()
                    if (body != null) continuation.resume(body)
                    else continuation.resumeWithException(
                        RuntimeException("response body is null"))
                }
                override fun onFailure(call: Call<T>, t: Throwable) {
                    continuation.resumeWithException(t)
                }
            })
        }
    }

调用如下:

    suspend fun getAppData() {
        try {
            val appList = ServiceCreator.create<AppService>().getAppData().await()
            // 对服务器响应的数据进行处理
        } catch (e: Exception) {
            // 对异常情况进行处理
        }
    }

只需要简单调用一下await()函数就可以让Retrofit发起网络请求,
并直接获得服务器响应的数据.

好了,到目前为止我们已经全面的学习了kotlin方方面面的知识,如果希望学习更多Kotlin知识,可以专门阅读《Kotlin编程权威指南》或者去AndroidDevelopers网站上学习更多知识,不过我认为你只要掌握了我所输出的这几篇内容,基本上已经可以应付开发过程中的问题了。

有问题请留言哈☺️

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

推荐阅读更多精彩内容