kotlin协程

114 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

协程可以简单的理解为轻量级的线程。(线程需要依靠操作系统的调度才能实现不同线程之间的切换,而使用协程就可以在编程语言的曾铭实现不同协程的切换,大大提升了并发编程的效率。)

协程的使用

添加依赖:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' //android中才会用到

创建CoroutinesTest.kt文件,定义main函数,开始使用协程:

fun main(){
	GlobalScop.launch{
    	println("codes run in coroutine scope")
    }
}

//代码不能打印出来,因为GlobalScop.launch创建的是顶层协程,这种协程当应用运行结束时也会跟着一起结束。所以日志没有打印出来,因为代码块的代码还没来得及运行。
//(我们将程序延迟一段时间就可以了。)
fun main(){
	GlobalScop.launch{
    	println("codes run in coroutine scope")
    }
    Thread.sleep(1000)
}

//但是这种程序如果在1s内不能运行结束,那么会被强制中断,比如:
fun main(){
	GlobalScop.launch{
    	println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")//新增打印日志
    }
    Thread.sleep(1000)
}

delay函数可以让当前协程延迟指定的时间后运行,它只会挂起当前协程,不会影响其他协程运行。 Thread.sleep方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。(注:delay函数只能在协程的作用域或其他挂起函数中调用) 所以我们新增的日志没有打印出来。

那有什么办法可以让应用程序在协程中所有代码都运行完了之后再结束呢?

借助runBlocking函数就可以实现。

fun main(){
	runBlocking{
    	println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")//新增打印日志
    }
}
//(注:runBlocking函数通常只在测试环境下使用,正式环境中使用容易产生一些性能上的问题)

如何创建多个协程?使用launch函数就可以了

fun main(){
	runBlocking{
        launch{
        	println("launch 1")
        	delay(1500)
        	println("launch 1 finished")
        }
		launch{
        	println("launch 2")
        	delay(1500)
        	println("launch 2 finished")
        }
    }
}
/**
(注:launch函数和runBlocking函数不同,它必须在协程的作用于中才能调用。其次它会在当前协程的作用余下创建子协程,子协程的特点是如果外层作用于的协程结束了,那么该作用域下的所有子协程也会一同结束。)
日志:
launch 1
launch 2
launch 1 finished
launch 2 finished
两个协程交替打印,这说明它们确实像多线程并发运行。
*/

挂起函数

随着launch函数中的逻辑越来越复杂,可能需要将部分代码提取到一个单独的函数中,但launch函数中编写的代码是拥有协程作用域的,但提取到单独函数中就没有作用域了,那么我们如何调用想delay这样的挂起函数呢? kotlin提供了一个suspend关键字,使用它可以将任意函数声明成挂起函数,挂起函数是可以互相调用的。

suspend fun printDot{
    println(.)
    delay(1000)
}

//suspend关键字只能将一个函数声明城挂起函数,是无法给他提供协程作用域的。比如尝试在printDot函数中调用launch函数,一定无法调用成功,因为launch函数要求必须在协程作用于中才能调用。
//我们可以借助coroutineScope函数来解决。coroutineScope函数是一个挂起函数,它的特点是继承外部的协程作用域并创建一个子作用域,我们就可以给任意挂起函数提供协程作用域了。
suspend fun printDot = coroutineScope{
    launch{
    	println(.)
    	delay(1000)
    }
}

//coroutineScope函数 和runBlocking函数有点类似,它可以保证作用域的所有代码和子协程代码在全部执行完之前,会一直阻塞当前协程
fun main(){
    runBlocking{
    	coroutineScope{
        	launch{
                for(i in 1..5){
                	println(.)
                    delay(1000)
                }
    		}
        }
        println("coroutineScope finished")
    }
    println("runBlocking finished")
}

/**
日志:
1
2
3
4
5
coroutineScope finished
runBlocking finished

coroutineScope函数只会阻塞当前协程,不影响其他协程,也不影响任何线程。因此不会造成任何性能上的问题。
runBlocking函数由于会阻塞当前线程,所以不推荐在实际项目中使用。
*/

更多的作用域构建器

  • GlobalScop.launch和runBlocking可以在任意地方调起

  • coroutineScope可以在协程作用域中或挂起函数汇总调用

  • launch只能在协程作用域中调用。

  • runBlocking由于会阻塞线程,因此只建议在测试环境中使用。

  • GlobalScop.launch每次创建的都是顶层协程,一般也不建议使用(除非非常明确创建顶层协程)

(为什么不建议使用顶层协程?因为它管理起来成本太高了。比如在activity中使用协程发起了一条网络请求,网络请求是耗时的,用户在服务器还没响应的情况下关闭了activity,按理来说应该取消这条网络请求,或者不进回调)

那么如何取消协程呢?

不管是GlobalScop.launch函数还是launch函数,他们都会返回一个Job对象,只需要调用Job对象的cancel方法就可以取消协程了。

val job = GlobalScop.launch{
	println("codes run in coroutine scope")
}
job.cancel()

//但是我们每次创建的都是顶层协程,activity关闭时,就需要逐个调用协程的cancel方法,那么代码无法维护。
//因此GlobalScop.launch这种协程作用域构建器在实际项目中也不太常用。比较常用的写法:
val job = Job()
vol scope = CoroutineScope(job)
scope.launch{

}
job.cancel()

创建一个协程,并返回它的执行结果?使用async实现。

async函数必须在协程作用域中才能调用,它创建一个个新的字写成并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await方法即可。

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

//调用了async函数后,代码块中的代码会立刻开始执行,当调动到await方法时,如果代码块中的代码还没有执行完,那么await方法会将当前协程阻塞住,直到可以获取async函数的执行结果。
fun main(){
	runBlocking{
        val start = System.currentTimeMillis()
    	val result = async{
            deleay(1000)
        	5+5
        }.await
        val result2 = async{
            deleay(1000)
        	5+6
        }.await
        println(result)
        val end = System.currentTimeMillis()
        println("cost $(end - start)ms")
    }
}
//两个async函数串行执行。


fun main(){
	runBlocking{
        val start = System.currentTimeMillis()
    	val F = async{
            deleay(1000)
        	5+5
        }
        val deferred2 = async{
            deleay(1000)
        	5+6
        }
        println(deferred1.await()+deferred2.await())
        val end = System.currentTimeMillis()
        println("cost $(end - start)ms")
    }
}
//在我们需要用到async函数的执行结果时才调用await()方法进行获取,这样两个async就会变成并行关系。

withContext函数

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

调用withContext函数之后,会立即执行代码块中的代码,同时将当前的协程阻塞主。当代码块中的全部代码执行完后,会将最后一行执行结果作为withContext函数的返回值返回。

相当于 val result = asyc{5+5}.await() 但不同的是,withContext函数强制要求指定一个线程参数。

线程参数有3种值可选。

  • Dispatchers.Default :默认为低并发的线程策略
  • Dispatchers.IO :高并发的线程策略。
  • Dispatchers.Main :android主线程。