1. 前言
关于协程,可能大家最经常听到的一句话就是“协程是轻量级的线程”。一脸懵逼,有没有?这可是官方的slogan,严格意义上讲,一方面官方是想让大家把协程和线程产生一个直观关联,另一方面想宣传协程在性能上比线程更优,充分地说服大家去使用它。本文我将尝试把协程是什么讲明白。
2. 聊聊线程
既然说“协程是轻量级的线程”。那我们有必要先回顾下线程是什么?在泛Java程序中,要启动一个线程那太easy了,new一个Thread,重写run方法,调用start方法,就是这么简单。
public fun thread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit ): Thread { val thread = object : Thread() { public override fun run() { block() } } if (isDaemon) thread.isDaemon = true if (priority > 0) thread.priority = priority if (name != null) thread.name = name if (contextClassLoader != null) thread.contextClassLoader = contextClassLoader if (start) thread.start() return thread }
简单是简单,不过也有不少弊端呢:
- 如果创建的线程数量超过了最大文件描述符数量,程序会报OOM的(当创建的线程的速度>线程消耗的速度时)
- 如果需要频繁创建线程去执行耗时非常短的代码,频繁的切换线程对性能也是有影响的
- 线程之间的通信比较复杂,把A线程的数据传递到B线程不那么容易
因为有了以上弊端,于是我们有了线程池。
3. 聊聊线程池
❝由于本文重点是讲协程,如果有同学对线程池不了解可以适当补补课,网上资料很多也不难。
❞
线程池想必大部分同学都很熟悉了。缓存池,对象池,连接池,各种池相关的技术就是缓存技术。线程池缓存的对象就是线程。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
我们可以看到线程池几个核心参数:
- corePoolSize核心线程池数量
- maximumPoolSize最大线程池数量
- BlockingQueue<Runnable> workQueue 工作队列,工作队列中保存的是Runnable对象
接下来再看下工作线程Worker的源码,它继承自Thread,它的run方法调用了runWorker方法,源码如下:
//ThreadPoolExecutor.java final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
我们看到该方法主要就是循环从workQueue中拿取可执行的runnable去执行。细心的同学可能会提出疑问了,如果while循环的条件不成立,那岂不是会导致线程直接退出。这种想法其实是个误区了,由于workQueue是BlockingQueue,如果队列中没有runnable对象,此处代码是会阻塞的,跳不出循环。
❝那么回到文章开头,"协程是轻量级的线程",到底何物比线程还要轻量级。对了聪明的读者可能已经猜出来了,workQueue中的runnable。为了方便理解,我们可以把协程理解为线程执行的最小单位,工作队列中的Runnable,有源码为证。
代码来自kotlinx-coroutines-core-jvm:1.4.1 //1. AbstractCoroutine public abstract class AbstractCoroutine<in T> (...): JobSupport(active), Job, Continuation<T>, CoroutineScope //2. DispatchedContinuation internal class DispatchedContinuation<in T>( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation<T> ) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation //3. DispatchedTask internal abstract class DispatchedTask<in T>( @JvmField public var resumeMode: Int ) : SchedulerTask() internal actual typealias SchedulerTask = Task //4.Task internal abstract class Task( @JvmField var submissionTime: Long, @JvmField var taskContext: TaskContext ) : Runnable { constructor() : this(0, NonBlockingContext) inline val mode: Int get() = taskContext.taskMode // TASK_XXX }
上述源码也可以简化为
public abstract class AbstractCoroutine<in T> (...): Runnable
简单讲协程就是一个Runnable,而且这个Runnable必须是存储在工作队列中,才能发挥它轻量级的优势。
❝题外话:线程,死循环,队列,MessageQueue。Android开发者最熟悉的MainThread不正天然的满足这些特性吗。难道说往Handler中post一个Runnable也是启动一个协程吗?如果这样类比能够让你更容易理解协程,那就这样理解吧,这样理解也没问题。只不过协程能做的远比往主线程post一个线程最小单位多多了。
❞
既然线程池,MainThread已经充分地发挥了线程的性能。那么为什么还要有协程呢?协程在他们之上又解决了什么问题呢?
4. 聊聊协程
首先来看一个最简单的例子,在Activity中开启一个协程,然后在子线程中休眠10s,结束后在主线程中打印出子线程中返回的值。
//TestActivity.java MainScope().launch { val result = withContext(Dispatchers.IO) { Thread.sleep(10_000) println("I am running in ${Thread.currentThread()}") "Hello coroutines" } println("I am running in ${Thread.currentThread()} result is $result") }
打印结果如下,我们看到在子线程中睡眠,在主线程中打印子线程中返回的值。
2021-11-22 22:29:02.868 3407-3463/com.peter.viewgrouptutorial I/System.out: I am running in Thread[DefaultDispatcher-worker-1,5,main] 2021-11-22 22:29:02.874 3407-3407/com.peter.viewgrouptutorial I/System.out: I am running in Thread[main,5,main] result is Hello coroutines
咋一看,大家可能会有疑问了,老兄,实现这种需求,有必要这么复杂吗,老弟我三下五除二搞定好吗?看我的
thread { Thread.sleep(10_000) println("I am running in ${Thread.currentThread()}") val result = "Hello coroutines" Handler(Looper.getMainLooper()).post { println("I am running in ${Thread.currentThread()} result is $result") } }
轻轻松松几行代码搞定,稳重而且不失风度,打印结果一模一样。
2021-11-22 22:35:59.016 3597-3655/com.peter.viewgrouptutorial I/System.out: I am running in Thread[Thread-3,5,main] 2021-11-22 22:35:59.020 3597-3597/com.peter.viewgrouptutorial I/System.out: I am running in Thread[main,5,main] result is Hello coroutines
那么问题来了,如果需求是在子线程中睡眠10s,将返回值返回给另一个子线程呢?当然用传统的线程也不是不能实现,如果用协程那就相当简单了
// 为了模拟出效果,特意使用只有一个线程的线程池来当Dispatcher MainScope().launch(Executors.newFixedThreadPool(1).asCoroutineDispatcher()) { val result = withContext(Dispatchers.IO) { Thread.sleep(10_000) println("I am running in ${Thread.currentThread()}") "Hello coroutines" } println("I am running in ${Thread.currentThread()} result is $result") }
打印结果如下,注意看是两个不同的线程
:
2021-11-22 22:41:01.953 3872-3927/com.peter.viewgrouptutorial I/System.out: I am running in Thread[DefaultDispatcher-worker-1,5,main] 2021-11-22 22:41:01.960 3872-3926/com.peter.viewgrouptutorial I/System.out: I am running in Thread[pool-1-thread-1,5,main] result is Hello coroutines
5. 总结
所以在我看来,协程有以下几个特性:
- 将协程体封装成线程可执行的最小单位Runnable,准确讲是协程中的Continuation,通过分发机制分发到对应的线程对应的工作队列中
- Continuation会保存协程栈帧中的数据,在切换线程时把协程栈帧带过去,在切回线程时,又通过它把数据带回来。(没错,类似callback机制)
- 线程池对开发者封装了线程,只需要往里面submit Runnable就可以了。而协程同时对开发者封装了线程和Callback,开发者无需关心线程和线程切换的内在逻辑。
//TestActivity.java MainScope().launch { val result = withContext(Dispatchers.IO) { Thread.sleep(10_000) println("I am running in ${Thread.currentThread()}") "Hello coroutines" } println("I am running in ${Thread.currentThread()} result is $result") }
val coroutinesBodyRunnable = java.lang.Runnable { thread { Thread.sleep(10_000) println("I am running in ${Thread.currentThread()}") val result = "Hello coroutines" Handler(Looper.getMainLooper()).post { println("I am running in ${Thread.currentThread()} result is $result") } } } Handler(Looper.getMainLooper()).post(coroutinesBodyRunnable)
以上代码是等价的。时间原因,具体原理,后续再讲,敬请期待。如果觉得文章有帮助,帮我分享给周围的朋友吧。期待我们可以在评论中碰撞出更多的火花,一起探讨技术,一起进步。
邀请三位好友关注我的公众号或者将此文转发到朋友圈保留一天,截图发送给我,送一本Kotlin Coroutines电子书籍。这本书是目前看到最好的一本协程入门,深入皆可的书。最好的,没有之一。