最近刚刚接触到协程,网上也翻阅了大量其他作者的博文来看,总感觉越看越不透彻,所以特此记录自己对于kotlin协程的理解和认识,如果有误,望指正


目录

  • 协程定义
  • 创建协程
  • 1.launch:Job
  • 1.1协程上下文
  • 1.2启动模式
  • 1.2.1 CoroutineStart .DEFAULT
  • 1.2.2 CoroutineStart .UNDISPATCHED
  • 1.2.3 CoroutineStart .LAZY
  • 1.3协程体
  • 2.async:Deferred
  • 3.withContext
  • 4.runBlocking
  • 扩展
  • 同步异步的含义
  • 阻塞非阻塞的含义


协程定义

官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

创建协程

kotlin 里没有 new ,自然也不像 JAVA 一样 new Thread,另外 kotlin 里面提供了大量的高阶函数,所以不难猜出协程这里 kotlin 也是有提供专用函数的。kotlin 中 GlobalScope 类提供了几个携程构造函数:

  • launch - 创建协程
  • async - 创建带返回值的协程,返回的是 Deferred 类
  • withContext - 不创建新的协程,在指定协程上运行代码块
  • runBlocking- 不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会
    kotlin 在 1.3 之后要求协程必须由 CoroutineScope 创建,CoroutineScope 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。比如 CoroutineScope.launch{} 可以看成 new Coroutine

1.launch:Job

这是最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。下面先看一下简单的使用:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.e(TAG, "主线程id:${mainLooper.thread.id}")
    val job = GlobalScope.launch {
        delay(6000)
        Log.e(TAG, "协程执行结束 -- 线程id:${Thread.currentThread().id}")
    }
    Log.e(TAG, "主线程执行结束")
}

//Job中的方法
job.isActive
job.isCancelled
job.isCompleted

job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动
job.join() - 等待协程执行完毕
job.cancel() - 取消一个协程
job.cancelAndJoin() - 等待协程执行完毕然后再取消

执行的结果

E/test: 主线程id:2
E/test: 主线程执行结束
E/test: 协程执行结束 -- 线程id:24613

launch的源码:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

从上可以看出
从方法定义中可以看出,launch() 是CoroutineScope的一个扩展函数,CoroutineScope简单来说就是协程的作用范围。launch方法有三个参数:1.协程下上文;2.协程启动模式;3.协程体: block是一个带接收者的函数字面量,接收者是CoroutineScope

1.1协程上下文

上下文可以有很多作用,包括携带参数,拦截协程执行等等,多数情况下我们不需要自己去实现上下文,只需要使用现成的就好。上下文有一个重要的作用就是线程切换Kotlin协程使用调度器来确定哪些线程用于协程执行,Kotlin提供了调度器给我们使用:

  • Dispatchers.Main:使用这个调度器在 Android 主线程上运行一个协程。可以用来更新UI 在UI线程中执行
  • Dispatchers.IO:这个调度器被优化在主线程之外执行磁盘或网络 I/O。在线程池中执行
  • Dispatchers.Default:这个调度器经过优化,可以在主线程之外执行 cpu 密集型的工作。例如对列表进行排序和解析 JSON。在线程池中执行。
  • Dispatchers.Unconfined:在调用的线程直接执行。

1.2启动模式

在Kotlin协程当中,启动模式定义在一个枚举类中:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    ATOMIC,
    UNDISPATCHED;
}

一共定义了4种启动模式,详情介绍:

  • 缺省 默认为DEFAULT
  • DEFAULT 立即等待被调度执行()
  • ATOMIC 立即等待被调度执行,并且开始执行前无法被取消,直到执行完毕或者遇到第一个挂起点 - suspend
  • UNDISPATCHED 立即在当前线程执行协程体内容
  • LAZY 需要手动触发才会进入等待调度

这里的立即等待被执行就是当当前协程处于空闲状态下才会去执行

1.2.1 CoroutineStart .DEFAULT
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG,"start")
        //new Coroutine 实例化协程
        GlobalScope.launch{
             Log.d(TAG,"work")
            //delay(200)
            launch(start = CoroutineStart.DEFAULT) {

                Log.d(TAG,"pass")
            }

            Log.d(TAG,"over")
            delay(300)
            Log.d(TAG,"finish")
        }

        Log.d(TAG,"end")

    }
2020-11-08 22:27:14.197 10613-10613/com.example.coroutinesdemo D/test: start
2020-11-08 22:27:14.236 10613-10613/com.example.coroutinesdemo D/test: end
2020-11-08 22:27:14.238 10613-10652/com.example.coroutinesdemo D/test: work
2020-11-08 22:27:14.241 10613-10652/com.example.coroutinesdemo D/test: over
2020-11-08 22:27:14.244 10613-10651/com.example.coroutinesdemo D/test: pass
2020-11-08 22:27:14.589 10613-10653/com.example.coroutinesdemo D/test: finish
1.2.2 CoroutineStart .UNDISPATCHED
GlobalScope.launch{
             Log.d(TAG,"work")
            //delay(200)
            launch(start = CoroutineStart.UNDISPATCHED) {
                Log.d(TAG,"pass")
            }
            Log.d(TAG,"over")
            delay(300)
            Log.d(TAG,"finish")
        }
        Log.d(TAG,"end")
    }
2020-11-08 22:33:20.477 11325-11325/com.example.coroutinesdemo D/test: start
2020-11-08 22:33:20.513 11325-11325/com.example.coroutinesdemo D/test: end
2020-11-08 22:33:20.515 11325-11364/com.example.coroutinesdemo D/test: work
2020-11-08 22:33:20.519 11325-11364/com.example.coroutinesdemo D/test: pass
2020-11-08 22:33:20.520 11325-11364/com.example.coroutinesdemo D/test: over
2020-11-08 22:33:20.851 11325-11364/com.example.coroutinesdemo D/test: finish
1.2.3 CoroutineStart .LAZY
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG,"start")
        //new Coroutine 实例化协程
        GlobalScope.launch{
             Log.d(TAG,"work")
            //delay(200)
            var job = launch(start = CoroutineStart.LAZY) {
                delay(200)
                Log.d(TAG,"pass")
            }
            //lazy需要手动触发,让其协程进入等待调度阶段 和DEFAULT效果一样
            job.start()

            //join会堵塞当前协程等待job协程执行完毕  和 UNDISPATCHED 有差别
            //job.join()

            Log.d(TAG,"over")
            delay(300)
            Log.d(TAG,"finish")
        }

        Log.d(TAG,"end")

    }

job.start()

2020-11-08 22:43:51.526 12562-12562/com.example.coroutinesdemo D/test: start
2020-11-08 22:43:51.536 12562-12562/com.example.coroutinesdemo D/test: end
2020-11-08 22:43:51.537 12562-12602/com.example.coroutinesdemo D/test: work
2020-11-08 22:43:51.538 12562-12602/com.example.coroutinesdemo D/test: over
2020-11-08 22:43:51.781 12562-12606/com.example.coroutinesdemo D/test: pass
2020-11-08 22:43:51.845 12562-12603/com.example.coroutinesdemo D/test: finish

job.join()

2020-11-08 22:45:03.471 12693-12693/com.example.coroutinesdemo D/test: start
2020-11-08 22:45:03.511 12693-12693/com.example.coroutinesdemo D/test: end
2020-11-08 22:45:03.512 12693-12734/com.example.coroutinesdemo D/test: work
2020-11-08 22:45:03.766 12693-12735/com.example.coroutinesdemo D/test: pass
2020-11-08 22:45:03.783 12693-12737/com.example.coroutinesdemo D/test: over
2020-11-08 22:45:04.124 12693-12734/com.example.coroutinesdemo D/test: finish

job.join()会堵塞当前协程等待job协程执行完毕 后才会执行后面的代码

1.3协程体

协程体是一个用suspend关键字修饰的一个无参,无返回值的函数类型。被suspend修饰的函数称为挂起函数,与之对应的是关键字resume(恢复),注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。
suspend函数会将整个协程挂起,而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行
总结一下:

  • Kotlin 中规定:挂起函数只能在协程或者其他suspend函数中使用,其实就相当于挂起函数只能直接或间接地在协程中进行调用
  • suspend关键字只是起一个标识作用,用以表明被suspend修饰的函数(也即挂起函数)内部存在耗时操作,因此必须放置在协程中进行调用。
  • suspend关键字标识一个挂起点,挂起点具备挂起和恢复执行作用。当协程调用suspend函数时,会挂起当前协程,开启一个异步任务,当异步任务完成后,就会在当前挂起点恢复协程,继续该协程后续任务。
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG,"start")
        GlobalScope.launch {
            val token = getToken()
            Log.d(TAG,"token"+token)
            val userInfo = getUserInfo(token)
            Log.d(TAG,"userInfo"+userInfo)
            setUserInfo(userInfo)
            Log.d(TAG,"userInfofinish")
        }
        Log.d(TAG,"end")
        repeat(8){
            Log.e(TAG,"主线程执行$it")
        }

        Log.d(TAG,"finish")

    }

    private fun setUserInfo(userInfo: String) {
        Log.e(TAG, userInfo)
    }

    private suspend fun getToken(): String {
        delay(5000)
        return "token"
    }

    private suspend fun getUserInfo(token: String): String {
        delay(2000)
        return "$token - userInfo"
    }
2020-11-08 23:12:38.920 16679-16679/com.example.coroutinesdemo D/test: start
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo D/test: end
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行0
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行1
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行2
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行3
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行4
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行5
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行6
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo E/test: 主线程执行7
2020-11-08 23:12:38.924 16679-16679/com.example.coroutinesdemo D/test: finish
2020-11-08 23:12:43.928 16679-16717/com.example.coroutinesdemo D/test: tokentoken
2020-11-08 23:12:45.931 16679-16717/com.example.coroutinesdemo D/test: userInfotoken - userInfo
2020-11-08 23:12:45.931 16679-16717/com.example.coroutinesdemo E/test: token - userInfo
2020-11-08 23:12:45.931 16679-16717/com.example.coroutinesdemo D/test: userInfofinish

这里可以明显看出,suspend函数会将整个协程挂起(阻塞协程),而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行的 上一个不执行完毕下一个不执行

2.async:Deferred

async跟launch的用法基本一样,区别在于:async的返回值是Deferred,将最后一个封装成了该对象。async可以支持并发,此时一般都跟await一起使用

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG,"start")
        GlobalScope.launch {
            val result1 = GlobalScope.async {
                getResult1()
            }
            val result2 = GlobalScope.async {
                getResult2()
            }
            val result = result1.await() + result2.await()
            Log.e(TAG,"result = $result")
        }
        Log.d(TAG,"finish")

    }
    private suspend fun getResult1(): Int {
        delay(3000)
        return 1
    }

    private suspend fun getResult2(): Int {
        delay(4000)
        return 2
    }
2020-11-09 09:08:08.923 26272-26272/com.example.coroutinesdemo D/test: start
2020-11-09 09:08:08.938 26272-26272/com.example.coroutinesdemo D/test: finish
2020-11-09 09:08:12.948 26272-26311/com.example.coroutinesdemo E/test: result = 3

async是不阻塞线程的,也就是说getResult1和getResult2是同时进行的,所以获取到result的时间是4s,而不是7s
当然我们还可以显示指定async返回值

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG,"start")
        GlobalScope.launch {
            val result1 = GlobalScope.async {
                getResult1()
                return@async "a"
            }
            val result2 = GlobalScope.async {
                getResult2()
                return@async "t"
            }
            val result = result1.await() + result2.await()
            Log.e(TAG,"result = $result")
        }
        Log.d(TAG,"finish")

    }
    private suspend fun getResult1(): Int {
        delay(3000)
        return 1
    }

    private suspend fun getResult2(): Int {
        delay(4000)
        return 2
    }
2020-11-09 09:31:05.404 31585-31585/com.example.coroutinesdemo D/test: start
2020-11-09 09:31:05.441 31585-31585/com.example.coroutinesdemo D/test: finish
2020-11-09 09:31:09.501 31585-31627/com.example.coroutinesdemo E/test: result = at

3.withContext

withContext 不创建新的协程,在指定协程上运行代码块

我们先来个有意思的测试,直接在我们的代码中声明withContext看看是否可以运行成功

android kotlin协程实现同步 kotlin协程 原理_java


可以看到直接爆红了 哈哈 来看一下错误提示

android kotlin协程实现同步 kotlin协程 原理_kotlin_02


这里可以看到提示说挂起函数withContext只能被调用在coroutine或者另一个挂起函数中

所以说如果要使用withContext我们就必须将他放入coroutine或者挂起函数中

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "start")

        GlobalScope.launch{
            val time1 = System.currentTimeMillis()

            val task1 = withContext(Dispatchers.IO) {
                delay(2000)
                Log.e(TAG, "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                "one"  //返回结果赋值给task1
            }
            val task2 = withContext(Dispatchers.IO) {
                delay(1000)
                Log.e(TAG, "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                "two"  //返回结果赋值给task2
            }
            Log.e(
                TAG,
                "task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]"
            )
        }
        Log.d(TAG, "finish")
    }
2020-11-09 10:43:34.912 17381-17381/com.example.coroutinesdemo D/test: start
2020-11-09 10:43:34.926 17381-17381/com.example.coroutinesdemo D/test: finish
2020-11-09 10:43:36.936 17381-17419/com.example.coroutinesdemo E/test: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-1]
2020-11-09 10:43:37.941 17381-17428/com.example.coroutinesdemo E/test: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-5]
2020-11-09 10:43:37.947 17381-17420/com.example.coroutinesdemo E/test: task1 = one  , task2 = two , 耗时 3019 ms  [当前线程为:DefaultDispatcher-worker-2]

从上面结果可以看出,多个withConext是串行执行,如上代码执行顺序为先执行task1再执行task2,共耗时两个任务的所需时间的总和。这是因为withConext是个加粗样式 ,当运行到 withConext 时会将整个协程挂起(相当于阻塞整个协程),直到withConext执行完成后再执行下面的方法。所以withConext可以用在一个请求结果依赖另一个请求结果的这种情况。

如果同时处理多个耗时任务,且这几个任务都无相互依赖时,可以使用 async … await() 来处理,将上面的例子改为 async 来实现如下

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "start")

        GlobalScope.launch{
            val time1 = System.currentTimeMillis()
            val task1 = async(Dispatchers.IO) {
                delay(2000)
                Log.e(TAG, "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                "one"  //返回结果赋值给task1
            }

            val task2 = async(Dispatchers.IO) {
                delay(1000)
                Log.e(TAG, "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                "two"  //返回结果赋值给task2
            }

            Log.e(TAG, "task1 = ${task1.await()}  , task2 = ${task2.await()} , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")

        }
        Log.d(TAG, "finish")
    }
2020-11-09 10:52:04.749 17876-17876/com.example.coroutinesdemo D/test: start
2020-11-09 10:52:04.763 17876-17876/com.example.coroutinesdemo D/test: finish
2020-11-09 10:52:05.770 17876-17931/com.example.coroutinesdemo E/test: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
2020-11-09 10:52:06.771 17876-17931/com.example.coroutinesdemo E/test: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-1]
2020-11-09 10:52:06.773 17876-17933/com.example.coroutinesdemo E/test: task1 = one  , task2 = two , 耗时 2009 ms  [当前线程为:DefaultDispatcher-worker-3]

4.runBlocking

launch和runBlocking区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "start")
            runBlocking {
                // 阻塞1s
                delay(1000)
                Log.d(TAG, "this is a coroutine")
            }

            // 阻塞2s
            Thread.sleep(2000)
            Log.d(TAG, "main end")
        Log.d(TAG, "finish")
    }
2020-11-09 11:08:03.509 19555-19555/com.example.coroutinesdemo D/test: start
2020-11-09 11:08:04.524 19555-19555/com.example.coroutinesdemo D/test: this is a coroutine
2020-11-09 11:08:06.525 19555-19555/com.example.coroutinesdemo D/test: main end
2020-11-09 11:08:06.525 19555-19555/com.example.coroutinesdemo D/test: finish

ok 到这里协程的启动方式就算结束了

扩展

协程经常拿来与线程进行对比,它们彼此很相似,但是也很不同。
可以简单理解如下:

  • 一个进程可以包含多个线程
  • 一个线程可以包含多个协程
    由于一个线程可以包含多个协程,而协程具备挂起和恢复功能,也因此让我们具备了在一个线程上执行多个异步任务的能力。
    同步与异步是针对返回结果来说的,
    对于同步调用,由调用者主动获取结果。
    对于异步调用,调用者是通过回调等方式被动获取结果的。

同步异步的含义

  • 同步:执行一个任务时,调用者调用后即可获取返回结果
  • 异步:执行一个任务时,调用者调用后直接返回,不关心结果,而是等到任务结束时,通过回调等方式通知调用者结果

简单理解,比如对于一个函数调用,
同步调用就是调用函数后,直接就可以获取结果。
异步调用就是调用函数后,不关心结果,等函数体内的任务结束时,通过回调等方式通知调用者结果。

阻塞非阻塞的含义

  • 阻塞:执行一个任务( 函数)时,当前调用线程调用后立即被挂起,无法执行后续代码
  • 非阻塞:执行一个任务时( 函数),当前调用线程调用后立即返回,可继续执行后续代码

阻塞和非阻塞是针对当前线程是否具备 CPU 执行权来说的,
对于阻塞调用,调用不立即返回,当前线程被挂起,失去 CPU 执行权,直至调用任务完成,返回结果。
对于非阻塞调用,调用立即返回,当前线程仍然拥有 CPU 执行权,可继续执行后续代码。

ok ,文章到此暂告一段落 (楼上装修要吵死了 脑瓜子嗡嗡的)
作者:Whyn