协程
可以简单地将它理解成一种轻量级的线程,线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。
Global.launch函数:
可以创建一个协程的作用域,每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。需要使用Thread.sleep()方法让主线程阻塞1秒钟,现在重新运行程序,日志可以正常打印出来了,否则代码块中的代码还没来得及运行,应用程序就结束了,无法看到打印的日志。
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
}
Thread.sleep(1000)
}
delay()与Thread.sleep()的区别:
delay()函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行。而Thread.sleep() 方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。注意,delay()函数只能在协程的作用域或其他挂起函数中调用。
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
Thread.sleep(1000)
//协程挂起1.5秒,但是主线程却只阻塞了1秒,只会打印一条结果,第二个println新增的日志没有打印出来。
}
runBlocking函数
fun main() {
runBlocking {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}
runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。
launch函数
fun main() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}
上面代码创建多个子协程,launch函数必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。
相比而言,GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。上面代码让多个协程运行在一个线程里面,完全由编程语言来调度,不需要操作系统的参与(线程需要),效率极高。
suspend关键字
使用它可以将任意函数声明成挂起函数,挂起函数之间都是可以互相调用的,suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。
coroutineScope函数
coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程。借助这个特性,可以给任意挂起函数提供协程作用域了:
suspend fun printDot() = coroutineScope {
launch {
println(".")
delay(1000)
}
}
coroutineScope函数和runBlocking函数有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。
fun main() {
runBlocking {
coroutineScope {
launch {
for (i in 1..10) {
println(i)
delay(1000)
}
}
}
println("coroutineScope finished")
}
println("runBlocking finished")
}
输出:
1
2
3
4
...
10
coroutineScope finished
runBlocking finished
使用runBlocking函数创建了一个协程作用域,然后调用coroutineScope函数创建了一个子协程。在coroutineScope的作用域中,我们又调用launch函数创建了一个子协程。
coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而runBlocking函数由于会挂起外部线程,如果你恰好又在主线程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。
现实项目中协程用法
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
// 处理具体的逻辑
}
job.cancel()
先创建了一个Job对象,然后把它传入CoroutineScope()函数当中,CoroutineScope()函数会返回一个CoroutineScope对象,调用它的launch函数来创建一个协程,现在所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消。平时测试还是使用:runBlocking函数
async函数
fun main() {
runBlocking {
val result = async {
5 + 5
}.await()
println(result)
}
}
async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,Deferred对象的await()方法可以获取async函数代码块的执行结果,也就是代码块最后一行做为返回值。
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(1000)
4 + 6
}.await()
println("result is ${result1 + result2}.")
val end = System.currentTimeMillis()
println("cost ${end - start} ms.")
}
}
result is 20.
cost 2032ms.
两个async函数是一种串行的关系,前一个执行完了后一个才能执行。在调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
5 + 5
}
val deferred2 = async {
delay(1000)
4 + 6
}
println("result is ${deferred1.await() + deferred2.await()}.")
val end = System.currentTimeMillis()
println("cost ${end - start} milliseconds.")
}
}
result is 20.
cost 1029 milliseconds.
不立刻调用await()方法,在需要用到async函数的执行结果时才调用await()方法进行获取,这样两个async函数就变成一种并行关系了。更加高效。
withContext()函数
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}
调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回。可以将它理解成async函数的一种简化版写法。
线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。
Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。
Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。
Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。
withContext()函数是强制要求指定以上参数,而其他函数则是可选的。