💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 协程的基本操作 ### 协程挂起 在协程程序中,CommonPool表示共享线程池,ForkJoinPool表示所有在该线程池中的线程可以尝试去执行其他线程创建的子任务,在这个线程池中很少有线程处于空闲状态。delay()函数类似于Thread.sleep(),都表示使程序延迟操作,但是delay()函数不会阻塞线程,只是挂起协程本身,当协程在等待时,线程将返回到线程池中,当协程等待完成时,将使用在线程池中的空闲线程来恢复程序继续执行。接下来我们通过一个案例来演示协程挂起的操作,具体代码如下所示。 ``` import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.launch fun main(args: Array<String>) { launch(CommonPool) { println("打印前的线程${Thread.currentThread()}") delay(1000L) println("hello") println("打印后的线程${Thread.currentThread()}") } //启动另外一个协程 launch(CommonPool) { delay(1000L) println("tom") } println("world") Thread.sleep(2000L) } ``` 运行结果: ``` world 打印前的线程Thread[ForkJoinPool.commonPool-worker-1,5,main] hello 打印后的线程Thread[ForkJoinPool.commonPool-worker-2,5,main] tom ``` 由于协程是由程序直接实现的一种轻量级线程,多线程的执行顺序是不固定的,字符串“hello”与“tom”是在两个协程中打印的,因此该程序的运行结果中打印的字符串“hello”与“tom”的顺序是不固定的。从运行结果可知,协程在延迟之前,第7行代码打印的当前执行的线程名称是“Thread[ForkJoinPool.commonPool-worker-1,5,main]”,当程序执行delay()函数之后就把协程挂起来了,当前执行的线程就进入到线程池中。当协程的延迟时间到了之后,程序会自动从线程池中找到一个空闲的线程把程序恢复,该程序中使用的恢复线程的名称是“Thread[ForkJoinPool.commonPool-worker-2,5,main]”,这个空闲线程是随机选取的,有可能还是线程“Thread[ForkJoinPool.commonPool-worker-1,5,main]”。如果将程序中的delay()函数替换为Thread.sleep(),则在这个程序中一直都用同一个线程,无论延时多久,都用“Thread[ForkJoinPool.commonPool-worker-1,5,main]”线程来执行程序,此时这个线程就是阻塞式的。 ### 挂起函数 挂起函数主要是通过suspend来修饰的一个函数,这个函数只能在协程代码内部调用,非协程代码不能调用。在前面小节中讲到delay()函数是一个挂起函数,当在程序中调用挂起函数时,在当前文件的左侧可以看到[插图]这样一个图标。一般出现这个图标时,就说明调用的是suspend修饰的挂起函数。除了API中提供的挂起函数之外,还可以自定义一个挂起函数。接下来我们通过suspend来创建一个挂起函数,具体代码如下所示。 ``` import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.launch //挂起函数 suspend fun hello() { delay(1000L) println("I am lucy") } fun main(args: Array<String>) { //协程代码调用挂起函数 launch(CommonPool) { hello() } Thread.sleep(2000L) //睡眠2000L } ``` 运行结果: ``` I am Lucy ``` 上述代码中,在第6行通过suspend创建了一个挂起函数hello(),然后在协程中调用挂起函数。需要注意的是,只有协程代码才可以调用挂起函数,非协程代码不能调用挂起函数hello(),如果调用,则编译器会报错。 ### 主协程 根据上一小节可知,挂起函数可以在协程代码中调用,由于主协程也属于协程,因此在主协程中也可以调用挂起函数。主协程主要用runBlocking来表示,主协程有两种表达方式,一种方式是通过“:Unit=runBlocking”定义,另一种方式是通过“runBlocking{}”代码块的形式定义。接下来我们通过一个案例来演示主协程的两种定义方式。 #### :Unit=runBlocking 使用:Unit=runBlocking定义主协程,通过主协程来延迟1秒打印world,具体代码如下所示。 ``` import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.runBlocking fun main(args: Array<String>):Unit= runBlocking { println("hello") delay(1000L) //睡眠1000秒钟打印world println("world") } ``` 运行结果: ``` hello world ``` #### runBlocking{} 使用runBlocking{}定义主协程,通过主协程来延迟1秒打印world,具体代码如下所示。 ``` import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.runBlocking fun main(args: Array<String>) { println("hello") runBlocking { delay(1000L) //睡眠1000L秒钟打印world } println("world") } ``` 运行结果: ``` hello world ``` ### 协程中的Job任务 协程可以通过launch()函数来启动,这个函数的返回值是一个Job类型的任务。这个Job类型的任务是协程创建的后台任务,它持有协程的引用,因此它代表当前协程的对象。获取Job类型的任务作用是判断当前任务的状态。Job任务的状态有3种类型,分别是New(新建的任务)、Active(活动中的任务)、Completed(已结束的任务)。Job任务中有两个字段,分别是isActive(是否在活动中)和isCompleted(是否停止),通过这两个字段的值可判断当前任务的状态。表9-1介绍了Job任务的3种状态。 | Job状态 |isActive | isCompleted | | --- | --- | --- | | New(新建的活动) | false | false | | Active(活动中) | true | false | | Completed (已结束) | false | true | 接下来我们通过一个案例来打印程序中任务的状态,具体代码如下所示。 ``` import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.launch fun main(args: Array<String>) { val job: Job = launch(CommonPool) { delay(1000L) println("hello") } println("主线程睡眠前:isActive=${job.isActive} isCompleted=${job.isCompleted}") println("world") Thread.sleep(2000L) //job.join(); println("主线程睡眠后:isActive=${job.isActive} isCompleted=${job.isCompleted}") } ``` 运行结果: ``` 主线程睡眠前:isActive=true isCompleted=false world hello 主线程睡眠后:isActive=false isCompleted=true ``` 上述代码中,协程函数launch()返回的是一个Job类型的任务,在主线程睡眠之前和睡眠之后分别打印这个Job类型的任务状态。根据运行结果可知,主线程睡眠之前,Job任务状态中的字段isActive的值为true,isCompleted的值为false,表示该任务是在活动中。主线程睡眠之后,Job任务状态中的字段isActive的值为false,isCompleted的值为true,表示该任务已经是结束状态。 >[success] **多学一招:协程中的join()函数** 如果去掉上面示例中的第13行代码后,程序运行的结果只会输出world字符串,不会输出hello字符串,这是因为协程中启动的是一个守护线程,当主线程完成之后,协程就会被销毁掉,因此协程中的代码就不会再执行。如果想输出hello字符串,除了使用Thread.sleep(2000L)语句之外,还可以使用Java线程中的join()函数,该函数表示的是当前任务执行完成之后再执行其他操作,也就是可以将上面示例中的第13行代码换成“job.join()”,此时由于join()函数是一个挂起函数,因此需要将上面示例中的main()函数设置为主协程,修改后的具体代码如下所示。 ``` import kotlinx.coroutines.experimental.* fun main(args: Array<String>):Unit= runBlocking { val job: Job = launch(CommonPool) { delay(1000L) println("hello") } println("主线程睡眠前:isActive=${job.isActive} isCompleted=${job.isCompleted}") println("world") job.join() //协程执行完之后再执行其他操作 println("主线程睡眠后:isActive=${job.isActive} isCompleted=${job.isCompleted}") } ``` 运行结果: ``` 主线程睡眠前:isActive=true isCompleted=false world hello 主线程睡眠后:isActive=false isCompleted=true ``` ### 普通线程和守护线程 在Kotlin中的线程分为普通线程和守护线程。普通线程是通过实现Thread类中的Runnable接口来创建的一个线程;守护线程就是程序运行时在后台提供通用服务的一种线程,例如垃圾回收线程就是一个守护线程。如果某个线程对象在启动之前调用了isDaemon属性并将其设置为true,则这个线程就变成了守护线程。接下来我们通过一个案例来演示如何将普通线程转化为守护线程,具体代码如下所示。 ``` class DaemonThread : Runnable { override fun run() { while (true) { println(Thread.currentThread().name + "---is running") } } } fun main(args: Array<String>) { println("main线程是守护线程吗?" + Thread.currentThread().isDaemon) val dt = DaemonThread() //创建一个DaemonThread对象dt val t = Thread(dt, "守护线程") //创建线程t共享dt资源 println("t线程默认是守护线程吗?" + t.isDaemon) //判断是否为守护线程 t.isDaemon = true //将线程t设置为守护线程 t.start() //调用start()方法开启线程t for (i in 1..3) { println(i) } } ``` 运行结果: ``` main线程是守护线程吗?false t线程默认是守护线程吗?false 1 2 3 守护线程——is running 守护线程——is running 守护线程——is running 守护线程——is running 守护线程——is running ``` 上述代码中,子线程t是一个普通线程,在开启线程t之前,设置其isDaemon的属性值为true,此时子线程t就变为了守护线程。当开启守护线程t之后,程序会执行死循环中的打印语句,当主线程死亡后,JVM会通知守护线程。由于守护线程从接收指令到做出响应需要一定的时间,因此打印了几次“守护线程——is running”语句后,守护线程结束。 >[info] **注意** 要将某个线程设置为守护线程,必须在该线程启动之前,也就是说isDaemon属性的设置必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。 ### 线程与协程效率对比 一般情况下,线程有很多缺点,例如当启动一个线程的时候需要通过回调方式进行异步任务的回调,代码写起来比较麻烦。而且启动线程时占用的资源比较多,启动协程时占用的资源相对来说比较少。接下来我们分别通过线程和协程来打印10万个点,来对比一下线程和协程所耗费的时间,也就是对比两者运行时效率的高低。 #### 线程执行效率 通过线程来打印100 000个点,并输出程序运行时耗费的时间,具体代码如下所示。 ``` fun main(args: Array<String>) { val start = System.currentTimeMillis() //获取开始时间 //创建线程 val list1 = List(100000, { Thread(Runnable { println(".") }) }) list1.forEach { it.start() //开启每个线程 } list1.forEach { it.join() //使每个线程执行完之后再执行后续操作 } val end = System.currentTimeMillis() //获取结束时间 println("总耗时:${end - start}") } ``` 运行结果: ``` 总耗时:20197 ``` #### 协程执行效率 通过协程来打印100 000个点,并输出程序运行时耗费的时间,具体代码如下所示。 ``` import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.runBlocking fun main(args: Array<String>):Unit=runBlocking{ val start = System.currentTimeMillis() //获取开始时间 //创建协程 val list2 = List(100000) { launch { println(".") } } list2.forEach { it.join() } val end = System.currentTimeMillis() println("总耗时:${end-start}") } ``` 运行结果: ``` 总耗时:958 ``` 根据上述两个文件的运行结果可知,协程的效率比线程要高很多。协程是依赖于线程存在的,协程启动之后需要在线程中来执行,但是启动100 000个协程并不代表启动了100 000个线程,协程可以将程序中的定时操作或者延时操作挂起来,启动100 000个协程可能只需要5、6个线程就可以完成。对于线程来说,启动100 000个线程,就需要这么多资源,并且线程之间进行切换时也需要耗费很多时间,因此线程的效率就比较低。