大连网页建站模板,江苏省住房和城乡建设部网站,校园网站建设的作用,传媒公司做网站编辑_如何?提示#xff1a;此文章仅作为本人记录日常学习使用#xff0c;若有存在错误或者不严谨得地方欢迎指正。 文章目录 一、Kotlin中的协程1.1 协程的基本用法1.1.1协程与协程作用域1.1.2 使用launch函数创建子协程1.1.3 通过suspend关键声明挂起函数1.1.4 coroutineScope函数 1.2… 提示此文章仅作为本人记录日常学习使用若有存在错误或者不严谨得地方欢迎指正。 文章目录 一、Kotlin中的协程1.1 协程的基本用法1.1.1协程与协程作用域1.1.2 使用launch函数创建子协程1.1.3 通过suspend关键声明挂起函数1.1.4 coroutineScope函数 1.2 更多的作用域构建器1.2.1 项目中创建协程的常用方法1.2.2 获取协程的返回值1.2.3 withContext函数 一、Kotlin中的协程
协程是Kotlin语言中很有代表性的一种并发设计模式用于简化异步执行的代码。协程和线程有点类似可以简单地将它理解成一种轻量级的线程。我们前面学习的线程是属于重量级的这是因为线程需要依靠操作系统的调度来实现不同线程之间的切换。而协程仅在编程语言的层面就能实现不同协程之间的切换无需操作系统的介入从而极大提高了并发编程的运行效率。 举一个具体的例子例如我们有foo()和bar()这两个方法
fun foo(){a()b()c()
}
fun bar(){x()y()z()
}在没有开启线程的情况下先调用foo()方法后调用bar()方法理论上结果一定是a()、b()、c()执行完了以后x()、y()、z()才能够得到执行。而如果在协程A中调用foo()方法在协程B中调用bar()方法。虽然它们仍运行在同一个线程中但在执行foo()方法时随时都有可能被挂起而去执行bar()方法同理在执行bar()方法时也随时都有可能被挂起转而继续执行foo()方法这就使得最终输出的结果变得不确定了。 可以看出协程允许我们在单线程模式下模拟多线程编程的效果代码执行时的挂起与恢复完全是由编程语言来控制的和操作系统无关。
1.1 协程的基本用法
如果我们需要在项目中使用协程功能需要在build.gradle.kts(:app)中添加以下依赖
dependencies {· · ·implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9) // 适用于Android项目implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9) // 适用于非Android项目
} 创建一个CoroutinesTest.kt文件并在其中定义一个main()函数然后在main()函数中使用GlobalScope.launch函数
fun main() {GlobalScope.launch {println(codes run in coroutine scope)}
}GlobalScope.launch函数可以创建一个协程的作用域这样代码块中的代码就是在协程中运行的了。按照我们的理解现在运行main()函数应该会打印一句话才对。可是当你运行main()函数后却发现控制台中没有任何日志输出
这是因为GlobalScope.launch函数每次创建的都是一个顶层协程当应用程序运行结束时顶层协程也会跟着一起结束。刚才的日志之所以无法打印出来正是因为代码块中的代码还没来得及运行应用程序就结束了。为了解决这个问题我们让程序延迟一段时间再结束就行了
fun main() {GlobalScope.launch {println(codes run in coroutine scope)}// 让主线程休眠1s(1s后再关闭应用程序)Thread.sleep(1000)
}可以看到在使用Thread.sleep(1000)让主线程休眠1s后日志可以正常打印出来了。 可是这种写法还是存在一些问题的如果代码块中的代码在1秒钟内不能运行结束那么就会被强制中断
fun main() {GlobalScope.launch {println(codes run in coroutine scope)delay(1500)println(codes run in coroutine finished)}Thread.sleep(1000)
}我们在代码块中加入了一个delay()函数并在它后面又追加了一条打印。delay()函数可以让当前协程延迟一段时间后再运行但它和Thread.sleep()方法不同。delay()函数只会挂起当前协程并不会影响其他协程的运行。而Thread.sleep()方法会阻塞当前的线程这样所有运行在该线程下的协程都会被阻塞。 注意delay()函数只能在协程的作用域或其他挂起函数中调用。 这里我们让协程挂起1.5s(打印第一行日志1.5s后再打印第二行日志)让主线程休眠1s(应用程序1s后结束)。重新运行程序你会发现代码块中新增加的一条日志并没有打印出来因为它还没来得及运行应用程序就已经结束了。为了解决这个问题我们可以借助runBlocking函数让应用程序在协程中所有代码都运行完了之后再结束
fun main() {runBlocking {println(codes run in coroutine scope)delay(1500)println(codes run in coroutine finished)}
}runBlocking函数同样会创建一个协程的作用域但它可以保证所有该协程作用域内的代码和子协程在执行完毕之前会一直阻塞当前线程。需要注意的是runBlocking函数通常只应该在测试环境下使用在正式环境中使用容易产生一些性能问题。重新运行程序可以看到两条日志都能够正常打印出来了
1.1.1协程与协程作用域
协程(Coroutine)是一种轻量级的并发单元可以在其执行过程中挂起并返回到其父协程或顶层协程。协程的主要特点是它们能够被挂起并恢复这使得它们可以用来实现并发和异步编程。协程作用域(Coroutine Scope)则是一种定义协程生命周期范围的对象。每个协程都必须在某个作用域内运行当作用域被销毁时它内部的所有协程都会被自动取消。协程作用域可以用来管理协程的启动、取消和结构化并发。
简单来说协程是轻量级的并发单元而协程作用域则是定义协程生命周期范围的对象它可以用来管理和控制协程的执行。
1.1.2 使用launch函数创建子协程
接下来我们学习一下如何使用launch函数创建多个协程
fun main() {runBlocking {launch {println(launch1)delay(1000)println(launch1 finished)}launch {println(launch2)delay(1000)println(launch2 finished)}}
}这里的launch函数和我们前面所使用的GlobalScope.launch函数不同。首先①launch函数必须在协程的作用域中才能调用其次②launch函数会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了该作用域下的所有子协程也会一同结束而GlobalScope.launch函数创建的永远是顶层协程。 我们在runBlocking函数结构中调用了两次launch函数也就相当于创建了两个子协程。重新运行程序结果如下 可以看到两个子协程中的日志是交替打印的说明它们确实是像多线程一样并发运行的。然而这两个协程实际却运行在同一个线程当中只是由编程语言来决定如何在多个协程之间进行调度。调度的过程完全不需要操作系统参与这也就使得协程的并发效率会更高。 为了直观的体验到协程在处理并发事件时的效率我们进行以下实验
fun main() {// 协程开始执行的时间val start System.currentTimeMillis()runBlocking {repeat(100000) {launch {println(.)}}}// 协程执行完毕的时间val end System.currentTimeMillis()println(协程执行时间${end - start}ms)
}我们使用repeat()函数创建了10万个协程并让每个协程打印一行日志然后记录下整个操作的耗时。重新运行程序可以看到我们仅耗时348毫秒这足以看出协程是多么的高效。试想一下如果我们开启的是10万个线程程序或许早都因为内存泄露而崩溃了。
1.1.3 通过suspend关键声明挂起函数
随着launch函数中的逻辑越来越复杂可能你需要将部分launch函数中的代码提取到一个单独的函数中。这个时候就产生了另一个问题我们在launch函数中编写的代码是拥有协程作用域的但是如果将其提取到一个单独的函数中就没有协程作用域了那么我们该如何调用像delay()这样的挂起函数呢 为此Kotlin提供了一个suspend关键字使用suspend关键字可以将任意函数声明成挂起函数而挂起函数之间都是可以相互调用的
fun main() {// 协程开始执行的时间val start System.currentTimeMillis()runBlocking {repeat(100000) {launch {printDot()}}}// 协程执行完毕的时间val end System.currentTimeMillis()println(协程执行时间${end - start}ms)
}// 通过suspend关键字将printDot()声明成挂起函数
suspend fun printDot() {println(.)delay(1000)
}这样printDot()函数就是一个挂起函数了我们也就可以在printDot()函数中调用delay()函数。但suspend关键字只能将一个函数声明成挂起函数却无法给他提供协程作用域的。例如你现在尝试在printDot()函数中调用launch函数一定是无法调用成功的因为launch函数要求必须在协程作用域当中才能调用。
1.1.4 coroutineScope函数
为了解决suspend关键字作用域的问题我们可以借助coroutineScope函数来解决。由于coroutineScope函数也是一个挂起函数因此可以在其他挂起函数中调用它。coroutineScope函数的特点是会继承外部协程的作用域并创建一个子协程借助这个特性我们可以给任意挂起函数提供协程作用域
suspend fun printDot() coroutineScope {launch {println(.)delay(1000)}
}通过在printDot()内部使用coroutineScope函数使得coroutineScope函数内部启动的协程由launch创建继承了printDot()函数的父作用域也就是调用printDot()函数的外部作用域。 可以看到现在我们可以在printDot()这个挂起函数中调用launch函数了。coroutineScope函数和runBlocking函数有点类似它可以保证其作用域内的所有代码和子协程在全部执行完之前外部的协程会一直被挂起。例如下面的示例代码
fun main() {// runBlocking确保其作用域内所有代码和子协程都执行完毕前会阻塞当前线程runBlocking {// coroutineScope确保其作用域内所有代码和子协程都执行完毕前会阻塞当前线程coroutineScope {launch {for (i in 1..10) {println(.)delay(1000)}}}println(coroutineScope finished)}println(runBlocking finished)
}我们先使用runBlocking函数创建了一个协程作用域然后这个主协程作用域内又调用了coroutineScope函数创建了另一个新的协程作用域并等待该作用域内的所有子协程执行完毕。运行结果如下 由此可见coroutineScope函数确实是将外部runBlocking协程挂起了。只有当coroutineScope作用域内所有的代码和子协程都执行完毕后才会执行它之后的代码。需要注意虽然coroutineScope函数和runBlocking函数很类似但coroutineScope函数只会阻塞当前协程既不会影响其他协程也不会影响任何线程因此是不会造成任何性能上的问题的。而runBlocking函数则会挂起外部线程如果你恰好又在主线程中调用runBlocking函数的话很可能会造成界面卡死所以不推荐在实际项目中体验。
1.2 更多的作用域构建器
在上一小节的内容中我们学习了GlobalScope.launch、runBlocking、launch、coroutineScope这几种作用域构建器。它们之间的调用方法还是有些许不同的
GlobalScope.launch{ }函数、runBlocking{ }函数可以在任意地方调用。实际项目中不推荐使用coroutineScope{ }函数可以在协程作用域或者挂起函数中调用。launch{ }函数只能在协程作用域中调用。
注意 ①由于runBlocking{ }函数会阻塞当前线程因此只建议在测试环境下使用不建议在实际项目中使用。 ②而GlobalScope.launch{ }函数每次创建的都是顶层协程(当应用程序结束时顶层协程也会结束)除非明确需要创建顶层协程否则也不建议在实际项目中使用。 这里将讲一下为什么不建议在实际项目中使用顶层协程 不建议使用的原因是它的管理成本太高了。例如我们在某个Activity中使用协程发起了一条网络请求由于网络请求是耗时的当用户在服务器还没来得及响应的情况下就关闭了当前Activity按理说此时应该取消这条网络请求或者不应该进行回调操作。因为Activity已经被用户关闭了此时就算服务器返回了数据也没有任何意义。 然而为了取消顶层协程不管是GlobalScope.launch{ }函数还是launch{ }函数它们都会返回一个Job对象我们需要调用Job对象的cancel()方法来取消协程
val job GlobalScope.launch {· · ·
}
job.cancel()如果我们每次创建的都是顶层协程那么当Activity关闭时我们就需要逐个调用所有已创建协程的cancel()方法这简直是一种灾难因此GlobalScope.launch{ }这种协程作用域构建器在实际项目中也不太常用。 1.2.1 项目中创建协程的常用方法
下面是实际项目中比较常见的写法
fun main() {// 创建Job对象val job Job()// 通过Job对象创建CoroutineScope对象val scope CoroutineScope(job)// 通过CoroutineScope对象的launch{}函数创建协程scope.launch {//处理具体逻辑}// 关闭协程job.cancel()
}我们先是创建了一个Job对象然后通过这个Job对象来创建一个CoroutineScope对象。之后我们就可以通过调用这个CoroutineScope对象的launch{ }函数来创建一个协程了。现在所有调用CoroutineScope的launch{ }函数所创建协程都会被关联在Job对象的作用域下面。现在我们只需要调用一次Job对象中的cancel()方法就可以将同一作用域内的所有协程全部取消从而很大程度上降低了协程管理的成本。 总而言之CoroutineScope( )函数更适合实际项目当中使用。如果你只是在Main()函数中编写一些学习测试用的代码还是使用runBlocking{ }函数最为方便。
1.2.2 获取协程的返回值
通过前面的学习你已经知道了调用launch{ }函数可以创建一个新的协程但是launch{ }函数只能用于执行一段逻辑却不能获取执行的结果因为他的返回值永远是一个Job对象。那么如何能够创建一个协程并获取它的执行结果呢 其实我们通过async函数就可以实现async函数必须在协程作用域当中才能调用。async函数会创建一个新的协程并返回一个Deferred对象如果我们想要获取async函数代码块的执行结果只需要调用Deferred对象的await()方法即可。下面是一段示例代码
fun main() {runBlocking {val result async {5 5}.await()println(result)}
}我们通过async()函数将运算结果保存了下来然后打印到日志中。 在调用了async函数之后代码块中的代码就会立刻开始执行。当调用await()方法时如果async函数体中的代码还没有执行完毕那么当前协程(即 runBlocking 函数内的协程)会被挂起直到可以获得async函数的执行结果。我们接下来编写一段代码进行验证
fun main() {runBlocking {// 开始时间val start System.currentTimeMillis()// result1延时1sval result1 async {delay(1000)5 5}.await()// result2延时1sval result2 async {delay(1000)4 6}.await()println(result is : ${result1 result2})// 结束时间val end System.currentTimeMillis()println(一共耗时 : ${end - start}ms)}
}这里我们使用了两个async函数来执行任务在每个async代码块中调用delay()方法进行1秒的延时然后在async代码块执行完毕后都调用了await()方法。按照我们之前的学习await()方法使得async代码块中的代码在执行完毕之前会一直阻塞当前协程。运行程序后可以看到如下打印足以说明协程是顺序执行的即等待result1执行完毕后再执行result2的。 但是上面这种写法效率是很低的因为每个async块执行完后都调用了await()方法。这意味着第一个async块中的代码在执行完毕后必须等待1秒后才去执行第二个async块。而第二个async块同样会等待1秒后才去执行主线程中的其他代码所以总的执行时间约为2秒。其实两个async函数完全可以同时执行从而提高运行效率。现在对上述代码进行优化
fun main() {runBlocking {val start System.currentTimeMillis()val deferred1 async {delay(1000)5 5}val deferred2 async {delay(1000)4 6}// 在使用返回值的地方再调用await()方法println(result is : ${deferred1.await() deferred2.await()})val end System.currentTimeMillis()println(一共耗时 : ${end - start}ms)}
}可以看到在这段代码中我们改变了调用await()函数的时机仅在需要用到async代码块的执行结果时才调用await()方法去获取。这样两个async函数就可以并行执行了第二个async再也不用等待第一个async完成之后才能执行了。这次重新运行程序 可以看到我们的代码耗时从2015ms缩短为了1015ms运行效率显著提升。这也就说明我们两个async中的代码确实是并行执行的并且成功将他们的结果输出到日志中了。
1.2.3 withContext函数
最后我们再来学习一个比较特殊的作用域构建器——withContext( )函数。withContext()函数也是一个挂起函数可以将其理解成async函数的一种简化版写法。示例代码如下
fun main() {runBlocking {val result withContext(Dispatchers.Default) {5 5}println(result)}
}当我们调用withContext()函数后会立即执行代码块中的代码同时会将外部协程(runBlocking{ })挂起。当代码块中的代码全部执行完毕后会将最后一行的执行结果作为withContext()函数的返回值进行返回。因此基本相当于val result async{ 5 5 }.await()的写法唯一不同的是withContext()函数会强制要求我们指定一个线程参数。 你已经知道协程是一种轻量级的线程因此很多传统编程情况下需要开启多线程执行并发任务。然而借助协程我们只需要在一个线程中开启多个协程来执行就可以了。这并不意味着我们就永远不需要开启线程了比如说Android中的网络请求必须要在子线程中进行即使你开启了协程去执行网络请求假如它是主线程当中的协程那么程序依然会报错。这个时候我们就应该通过线程参数给协程指定一个具体运行的线程。 线程参数主要有以下3种值可以选择Dispatchers.Default、Dispatchers.IO、Dispatchers.Main。
Dispatchers.Default使用一种默认低并发的线程策略当你要执行的代码属于计算密集型任务时开启过高的并发反而可能会影响任务的运行效率因此就可以使用Dispatchers.Default。Dispatchers.IO使用一种较高并发的线程策略当你要执行的代码大多数时间是在阻塞和等待中比如说要执行网络请求时为了能够支持更高的并发数量此时就可以使用Dispatchers.IO。Dispatchers.Main表示不会开启子线程而是在Android主线程中执行代码但是这个值只能在Android项目中使用纯Kotlin程序使用这种类型的线程参数会报错。
事实上除了coroutineScope{ }函数之外其他所有的函数都是可以指定这样一个线程参数的只不过withContext()函数是强制要求指定的而其他函数则是可选的。