抽丝剥茧聊Kotlin协程之认真聊聊协程是什么

简介: 抽丝剥茧聊Kotlin协程之认真聊聊协程是什么

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
}

简单是简单,不过也有不少弊端呢:


  1. 如果创建的线程数量超过了最大文件描述符数量,程序会报OOM的(当创建的线程的速度>线程消耗的速度时)
  2. 如果需要频繁创建线程去执行耗时非常短的代码,频繁的切换线程对性能也是有影响的
  3. 线程之间的通信比较复杂,把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);
}

我们可以看到线程池几个核心参数:


  1. corePoolSize核心线程池数量
  2. maximumPoolSize最大线程池数量
  3. 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. 总结


所以在我看来,协程有以下几个特性:


  1. 将协程体封装成线程可执行的最小单位Runnable,准确讲是协程中的Continuation,通过分发机制分发到对应的线程对应的工作队列中
  2. Continuation会保存协程栈帧中的数据,在切换线程时把协程栈帧带过去,在切回线程时,又通过它把数据带回来。(没错,类似callback机制)
  3. 线程池对开发者封装了线程,只需要往里面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)


以上代码是等价的。时间原因,具体原理,后续再讲,敬请期待。如果觉得文章有帮助,帮我分享给周围的朋友吧。期待我们可以在评论中碰撞出更多的火花,一起探讨技术,一起进步。


相关文章
|
8天前
|
传感器 Android开发 开发者
构建高效Android应用:Kotlin的协程与Flow
【4月更文挑战第26天】随着移动应用开发的不断进步,开发者寻求更简洁高效的编码方式以应对复杂多变的业务需求。在众多技术方案中,Kotlin语言凭借其简洁性和强大的功能库逐渐成为Android开发的主流选择。特别是Kotlin的协程和Flow这两个特性,它们为处理异步任务和数据流提供了强大而灵活的工具。本文将深入探讨如何通过Kotlin协程和Flow来优化Android应用性能,实现更加流畅的用户体验,并展示在实际开发中的应用实例。
|
2月前
|
安全 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美结合
【2月更文挑战第30天】在移动开发领域,性能优化和流畅的用户体验是关键。本文深入探讨了如何通过结合Kotlin语言和协程技术来提升Android应用的性能和响应能力。我们将分析Kotlin的优势,介绍协程的基本概念,并通过实际案例展示如何在应用中实现协程以简化异步编程,从而提供更加高效的解决方案。
|
2月前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin与协程的完美融合
【2月更文挑战第25天】 在移动开发领域,性能优化和应用响应性的提升是永恒的追求。随着Android Jetpack组件库的不断丰富,Kotlin语言已经成为Android开发的首选。而Kotlin协程作为一种新的并发处理方案,它以轻量级线程的形式,为开发者提供了简洁高效的异步编程手段。本文将深入探讨Kotlin协程在Android应用中的实践运用,以及如何通过这种技术改善用户界面的流畅度和后台任务的处理能力,进而构建出更高效、更稳定的Android应用。
|
4天前
|
算法 安全 Android开发
深入理解操作系统的内存管理机制构建高效Android应用:Kotlin的协程优势
【4月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键。本文将探讨操作系统内存管理的核心技术,包括内存分配、虚拟内存、分页和分段等概念,以及它们是如何协同工作以提高内存利用率和系统性能的。通过对这些技术的详细分析,我们可以更好地理解操作系统背后的原理,并评估不同内存管理策略对系统行为的影响。 【4月更文挑战第30天】 在移动开发领域,尤其是针对Android平台,性能优化和流畅的用户体验始终是开发者追求的核心目标。随着Kotlin语言的普及,协程作为其在异步编程领域的杀手锏特性,已经逐渐成为提高应用性能和简化代码结构的重要工具。本文将深入探讨Kotli
|
6天前
|
数据处理 调度 Android开发
构建高效Android应用:探究Kotlin的协程优势
【4月更文挑战第28天】 随着移动应用开发日趋复杂,传统的线程和异步处理模型在处理并发任务时显得笨重且易出错。本文深入分析了Kotlin协程作为一种轻量级线程管理方案的优势,探讨了其在Android应用中实现流畅用户界面和提升性能的潜力。通过对协程原理的剖析以及与现有技术的比较,揭示了如何利用协程优化应用结构、简化代码逻辑,并提高应用的响应性和可维护性。
|
20天前
|
数据库 Android开发 UED
构建高效Android应用:Kotlin与协程的完美结合
【4月更文挑战第14天】在现代Android开发中,Kotlin语言和协程已经成为提升应用性能和简化异步编程的关键技术。本文深入探讨了如何通过结合Kotlin的简洁语法和协程的轻量级并发能力来构建一个高效的Android应用。我们将分析协程的核心原理,展示其在处理网络请求、数据库操作和UI线程中的实际应用,并提供优化策略以确保流畅的用户体验和低资源消耗。
24 4
|
2月前
|
移动开发 调度 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【2月更文挑战第21天】随着移动开发技术的不断进步,Android平台正寻求更高效、更简洁的编程解决方案。Kotlin作为一种现代编程语言,以其简洁性和对函数式编程的支持赢得了开发者的青睐。而协程作为处理异步任务的强大工具,在提升应用性能方面显示出巨大潜力。本文将深入探讨Kotlin语言和协程技术如何相辅相成,帮助开发者构建更加流畅和高效的Android应用。
|
2月前
|
API 数据库 Android开发
构建高效安卓应用:Kotlin与协程的完美融合
【2月更文挑战第20天】在移动开发领域,性能优化和资源管理始终是开发者关注的焦点。随着Kotlin语言在Android平台的普及,协程作为其核心特性之一,提供了一种全新的异步编程范式。本文将深入探讨如何通过Kotlin协程提升安卓应用的性能,减少内存消耗,并实现流畅的用户体验。我们将分析协程的原理,展示其在实际应用中的效能,并提供实践指南,帮助开发者掌握这一强有力的工具。
15 0
|
2月前
|
安全 Java Android开发
构建高效Android应用:Kotlin与协程的完美融合
【2月更文挑战第15天】随着移动开发技术的不断进步,Android平台的开发者寻求更高效的编程解决方案以提升应用性能和用户体验。Kotlin作为官方推荐的开发语言,以其简洁性和功能安全性受到青睐。而协程,作为一种轻量级的线程管理机制,在异步编程和网络请求等方面展现出巨大潜力。本文将深入探讨Kotlin语言结合协程如何优化Android应用的性能,包括提高响应性、简化异步代码结构以及减少资源消耗等方面,旨在为开发者提供实战指南,帮助他们构建更加流畅和高效的Android应用。
|
7月前
|
安全 调度 数据库
Kotlin 学习笔记(五)—— 协程的基础知识,面试官的最爱了~(下)
Kotlin 学习笔记(五)—— 协程的基础知识,面试官的最爱了~(下)
44 0