笔友城堡 - 可定义的个人主页

前言

记录一下Kotlin中协程的定义和协成的使用,主要是面试准备,简单记录于此,方便自己查阅和学习。

正文

协程

协程是Kotlin提供的轻量级并发方案,通过suspend挂起函数实现非阻塞异步编程,配合CoroutineScope和Dispatcher,在Android中用于安全地执行网络、数据库等异步任务。

协程主要还是运行在线程中的,因此协程不能代替线程

协程挂起

delay()函数类似于Thread.sleep(),都表示使程序延迟操作,但是delay()函数不会阻塞线程,只是挂起协程本身

当协程在等待时,线程将返回到线程池中,当协程等待完成时,将使用在线程池中的空闲线程来恢复程序继续执行。

Log.d(TAG, "1 打印前的线程${Thread.currentThread()}")
CoroutineScope(Dispatchers.IO).launch {
    Log.d(TAG, "2 打印前的线程${Thread.currentThread()}")
    delay(1000L)
    Log.d(TAG, "3 打印前的线程${Thread.currentThread()}")
}
// 启动另外一个协程
CoroutineScope(Dispatchers.IO).launch {
    delay(2000L)
    Log.d(TAG, "4 打印前的线程${Thread.currentThread()}")
}
Log.d(TAG, "5 打印前的线程${Thread.currentThread()}")
Thread.sleep(2000L)

打印日志

1 打印前的线程Thread[main,5,main]
5 打印前的线程Thread[main,5,main]
2 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
3 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
4 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
  1. 协程运行在线程中

    所以协程替代不了线程

  2. 协程延迟后,会从线程池中找一个空闲的线程恢复执行

    上面2个协程可能在同一个线程中执行

如果第二个协程delay时间改为delay(1000L)后,下面打印

//第一次
1 打印前的线程Thread[main,5,main]
5 打印前的线程Thread[main,5,main]
2 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
4 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
3 打印前的线程Thread[DefaultDispatcher-worker-2,5,main]
//第二次
1 打印前的线程Thread[main,5,main]
5 打印前的线程Thread[main,5,main]
2 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
4 打印前的线程Thread[DefaultDispatcher-worker-2,5,main]
3 打印前的线程Thread[DefaultDispatcher-worker-1,5,main]
挂起函数

通过suspend来修饰的一个函数就是挂起函数。

挂起函数只能在协程代码内部调用,非协程代码不能调用。

suspend fun sayYourName() {
    delay(1000L)
    Log.d(TAG, "sayYourName : I am biumall")
}
CoroutineScope(Dispatchers.IO).launch {
    sayYourName()
}

如果不在协程中调用会有如下提示

Suspend function 'suspend fun sayYourName(): Unit' can only be called from a coroutine or another suspend function.
主协程

主协程主要用runBlocking来表示,主协程有两种表达方式,一种方式是通过“:Unit=runBlocking”定义,另一种方式是通过“runBlocking{}”代码块的形式定义。

  1. :Unit=runBlocking

    使用:Unit=runBlocking定义主协程

    //使用:Unit=runBlocking
    override fun onResume():Unit=runBlocking {
        super.onResume()
        sayYourName()
    }

    也就是在函数后面添加了:Unit=runBlocking。

  2. runBlocking{}

    使用runBlocking{}定义主协程

    override fun onResume() {
        super.onResume()
        //使用runBlocking
        runBlocking {
            sayYourName()
        }
    }
协程中的Job任务

协程可以通过launch()函数来启动,这个函数的返回值是一个Job类型的任务。Job类型的任务是协程创建的后台任务,它持有协程的引用,因此它代表当前协程的对象。获取Job类型的任务作用是判断当前任务的状态。

Job任务的状态有3种类型,分别是New(新建的任务)、Active(活动中的任务)、Completed(已结束的任务)。

Job任务中有两个字段,分别是isActive(是否在活动中)和isCompleted(是否停止),通过这两个字段的值可判断当前任务的状态。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContentView(R.layout.activity_main)
    Log.d(TAG, "onCreate1 : ")
    val job: Job = CoroutineScope(Dispatchers.IO).launch {
        delay(1000L)
        Log.d(TAG, "onCreate2 : ")
    }
    Log.d(TAG, "onCreate3 主线程睡眠前:isActive=${job.isActive} isCompleted=${job.isCompleted}")
    Thread.sleep(2000L)
    Log.d(TAG, "onCreate4 主线程睡眠后:isActive=${job.isActive} isCompleted=${job.isCompleted}")
}

打印日志

onCreate1 : 
onCreate3 主线程睡眠前:isActive=true isCompleted=false
onCreate2 : 
onCreate4 主线程睡眠后:isActive=false isCompleted=true

###### 普通线程和守护线程

在Kotlin中的线程分为普通线程和守护线程。

普通线程是通过实现Thread类中的Runnable接口来创建的一个线程;

守护线程就是程序运行时在后台提供通用服务的一种线程,例如垃圾回收线程就是一个守护线程。

如果某个线程对象在启动之前调用了isDaemon属性并将其设置为true,则这个线程就变成了守护线程。

要将某个线程设置为守护线程,必须在该线程启动之前,也就是说isDaemon属性的设置必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。

线程与协程效率对比

一般情况下,线程有很多缺点,例如当启动一个线程的时候需要通过回调方式进行异步任务的回调,代码写起来比较麻烦。而且启动线程时占用的资源比较多,启动协程时占用的资源相对来说比较少。

协程取消

既然协程有启动,那么协程也就有取消。

在Kotlin中是通过cancel()方法将协程取消的。

val job: Job = CoroutineScope(Dispatchers.IO).launch {
    delay(1000L)
    Log.d(TAG, "launch : ")
}
//协程取消
job.cancel()
//或者
//协程中的cancel()函数和join()函数是可以进行合并的,合并之后是一个cancelAndJoin()函数,这个函数用于取消协程

除了job.cancel()还有job.cancelAndJoin()

PS: job.cancelAndJoin() 要再挂起函数中

Suspend function 'suspend fun Job.cancelAndJoin(): Unit' can only be called from a coroutine or another suspend function.
协程取消失效

一般情况下,一个协程需要通过cancel()方法来取消,这种取消方式只适用于在协程代码中有挂起函数的程序。

由于挂起函数在挂起时也就是等待时,该协程已经回到了线程池中,等待时间结束之后会重新从线程池中恢复出来,虽然可以通过cancel()方法取消这些挂起函数,但是在协程中调用某些循环输出数据的函数时,通过cancel()方法是取消不了这个协程的。

fun cancelFail(): Unit = runBlocking {
    Log.d(TAG, "cancelFail : ")
    val job = launch(Dispatchers.IO) {
        // 程序运行时当前的时间
        var nextTime = System.currentTimeMillis()
        while (true) {
            // 每一次循环的时间
            val currentTime = System.currentTimeMillis()
            if (currentTime > nextTime) {
                Log.d(TAG, "cancelFail 当前时间:${System.currentTimeMillis()}")
                nextTime += 1000L
            }
        }
    }
    delay(2000L)  // 使程序延迟2 秒
    Log.d(TAG, "cancelFail 协程取消前:isActive=${job.isActive}")
    job.cancel()  // 取消协程
    job.join()    // 执行完协程之后再执行后续操作
    Log.d(TAG, "cancelFail 协程取消后:isActive=${job.isActive}")
}

上面的就是取消失败,协程一直在打印。

可以通过下面几个方法解决取消失败:

  1. 通过对isActive值的判断来取消协程

    如果想要在结束协程时结束协程中的循环操作,则需要在循环代码中通过isActive的值来判断当前协程的状态,如果isActive的值为false,则表示当前协程处于结束状态,此时返回当前协程即可。

    if(!isActive) return@launch //返回当前协程
  2. 使用yield()挂起函数来取消协程

    还可以在循环代码中调用yield()挂起函数来结束协程中的循环操作,因为调用cancel()函数来结束协程时,yield()会抛出一个异常,这个异常的名称是Cancellation Exception,抛出这个异常之后协程中的循环操作就结束了,同时在循环代码中通过try…catch来捕获这个异常并打印异常名称,当捕获到这个异常之后将协程返回即可。

    try {
        yield()
    } catch (e: CancellationException) {
        Log.d(TAG, "cancelFail e :$e ")
        return@launch
    }

有点跟Thread退出类似哈

定时取消

协程中可以通过withTimeout()函数来限制取消协程的时间。

fun forWithTimeout() {
    Log.d(TAG, "forWithTimeout: ")
    runBlocking {
        val job = CoroutineScope(Dispatchers.Default).launch {
            withTimeout(2000L) {
                Log.d(TAG, "forWithTimeout: ")
                repeat(1000) { i ->
                    Log.d(TAG, "forWithTimeout I'm sleeping $i …")
                    delay(500L)
                }
            }
        }
        job.join()
    }
}
同步启动
fun callMethod(): Unit = runBlocking {
    val time = measureTimeMillis {
        //同步执行
        val a = doJob1()
        val b = doJob2()
        Log.d(TAG, "callMethod : a=$a b=$b")
    }
    Log.d(TAG, "callMethod:执行时间=$time")
}
//挂起函数doJob1()
suspend fun doJob1(): Int {
    Log.d(TAG, "doJob1: do")
    delay(1000L)
    Log.d(TAG, "doJob1: done")
    return 1
}
//挂起函数doJob2()
suspend fun doJob2(): Int {
    Log.d(TAG, "doJob2: do")
    delay(1000L)
    Log.d(TAG, "doJob1: done")
    return 2
}

日志打印

doJob1: do
doJob1: done
doJob2: do
doJob1: done
callMethod : a=1 b=2
callMethod:执行时间=2002
异步启动

异步启动通过async,跟上面差不多,修改点如下

fun callMethod(): Unit = runBlocking {
    val time = measureTimeMillis {
        //async执行
        val a = async { doJob1() }
        val b = async { doJob2() }
        Log.d(TAG, "callMethod : a=$a b=$b")
    }
    Log.d(TAG, "callMethod:执行时间=$time")
}

日志打印

callMethod : a=DeferredCoroutine{Active}@4b22740 b=DeferredCoroutine{Active}@3506679
callMethod:执行时间=1
doJob1: do
doJob2: do
doJob1: done
doJob1: done

对比

方式问题
Thread + Handler易内存泄漏、难取消
AsyncTask已废弃
RxJava学习成本高、易过度设计
协程简洁、可控、生命周期安全

参考文章

AI

如果你愿意,我可以再帮你整理:

  • 协程面试高频问答题

  • suspend 编译后的状态机图解

  • 协程 + Retrofit + Flow 标准写法模板

你可以直接说「要哪一个」。

相关网址

笔友城堡 - 可定义的个人主页

暂无评论

评论审核已启用。您的评论可能需要一段时间后才能被显示。

none
暂无评论...