Coroutine -- 未完待续

370 阅读7分钟

协程

概念

  • 协程本质上可以认为是运行在线程上的代码块,是一套有kotlin官方提供的线程API(类似Java的Executor) 不用过多关注线程也能写出并发操作。(线程框架)

  • 官方说协程说一种轻量级的线程,

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        repeat(100_000) { // launch a lot of coroutines
            launch {
                delay(5000L)
                print(".")
            }
        }
    
    import kotlin.concurrent.thread
    fun main() = runBlocking {
        repeat(100_000) { // launch a lot of coroutines
            thread {
                Thread.sleep(5000L)
                print(".")
            }
        }
    }
    

    It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot. Now, try that with threads. What would happen? (Most likely your code will produce some sort of out-of-memory error)

但是实际上,上面的thread是把所有内容放到了一个线程池,用协程和100000个threads进行比较性能,所以才会用thread才会导致内存不足,因为开启一个线程非常的消耗资源。同时,sleep()delay()本身也是不一样的,sleep()是占用线程,delay()不会占用线程

可用coroutine和executor进行比较,两个性能都差不多。两个都是基于线程造出来的工具包。

  • 总结:
    • 协程:切线程
    • 挂起:可自动切回来的的切线程
    • 非阻塞式挂起:用看起来阻塞的代码写非阻塞。

好处

  1. 方便(基于kolin的语言优势,所有用起来会比基于Java的方案会更方便一些)
  2. 可以用看起来同步方式写出异步代码,不需要回调
//回调方式
api.getCard(canId)
		.enqueue(obj : Callback<Bitmap> {
	...
	override fun onResponse(call:Call<Bitmap>,
													response: Resoinse<Bitmap>) {
		val card = resonse.body()
		api.getSofList(canId)
				.enqueue(obj : Callback<Bitmap> {
			... 
			override fun onResponse(call:Call<Bitmap>,
															response: Resoinse<Bitmap>) {
				val sofList = response.body()
				show(suspendMerge(cardDetail, sofList))
		})
	}
})

api.getUser(new Callback<User>() {
    @Override
    public void success(User user) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                nameTv.setText(user.name);
            }
        })
    }
    
    @Override
    public void failure(Exception e) {
        ...
    }
});
//协程
launch(Dispatchers.Main) {
	val cardDetail = async {api.getCard(canId)} // 获取卡详情
	val sofList = async {api.getSofList(canId)} // 获取银行卡信息
	val cardInfo = suspendMerge(cardDetail.await, sofList.await) // 合并两个信息
	show(cardInfo)  // UI显示
}

👆的两个网络请求是可以并行的网络请求,只是他们需要一起展示出来。如果用回调的方法,就会被做成一个串行的,然后网络等待多了一倍。而kotlin的协程,只用写为上下两行代码结构,然后在第三行合并。

如果网络请求更复杂,协程依然可以很清晰的写出来。 消除了并发任务间的协作难度,能很简单的实现并行

如何创建

launch async

runBlocking : 线程阻塞式的,试用与单元测试场景

标题asynclaunch
相同启动协程,返回Coroutine启动协程,返回Coroutine
不相同返回的Coroutine多实现了Deferred接口
  • launch

launch(Dispatchers.IO){} 创建一个新的协程,并在IO线程上执行它,

协程就是 {} 里面的代码

launch(Dispatchers.Main){
        withContext(Dispatchers.IO) { ... }
        withContext(Dispatchers.IO) { ... }
}

//切换线程也可以通过上下两行代码结构实现,不用嵌套
//还可以用suspend函数将withContext(Dispatchers.IO)提出到另外一个方法
suspend fun getCard() = withContext(Dispatchers.IO) {
        api.getCard()
}
launch(Dispatchers.Main){
        withContext(Dispatchers.IO) { ... }
        getCard()
}

withContext 这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行.

// 用 withContext 和 不用的区别
coroutineScope.launch(Dispatchers.IO) {
    ...
    launch(Dispatchers.Main){
        ...
        launch(Dispatchers.IO) {
            ...
            launch(Dispatchers.Main) {
                ...
            }
        }
    }
}

// 通过第二种写法来实现相同的逻辑
coroutineScope.launch(Dispatchers.Main) {
    ...
    withContext(Dispatchers.IO) {
        ...
    }
    ...
    withContext(Dispatchers.IO) {
        ...
    }
    ...
}
  • Async
coroutineScope.launch(Dispatchers.Main) {
      val card: Deferred = async { api.getCard(canId) }    
      val sofList: Deferred = async { api.getSofList(canId)} 
            // update UI
      show(sofList.await(), card.await())                     
}

//await的签名
public suspend fun await(): T

挂起

  1. 挂起的对象是协程

  2. launch(){} 里面的代码就是协程,当执行到某个suspends函数/挂起函数时, 这个协程就被挂起了 → 从正在执行他的线程上脱离出来。→ 从这一行代码开始,这个协程所在的线程不再运行这个协程了。

    • 然后该线程

      启动一个在主线程的协程,实际上就是往主线程post了一个任务 Runnable(即协程代码),那当协程挂起以后,实际上就是post()这个任务提前结束了。这个时候主线程就该做什么做什么,例如继续刷新界面(安卓的主线程),或者就被回收/再利用(后台任务)

      launch(Dispatchers.Main) {
      	val user = suspendGetCard(canId)
      	status = card.status
      }
      
      //相当于
      handler.post{
      	val card = api.getCard(canId)
      	status = card.status
      }
      
    • 协程

      函数代码在运行到挂起函数/suspend函数的时候被掐断了,所有接下来会从挂起函数开始继续向下在指定的线程里执行,

      launch(Dispatchers.Main) {
      	val card = suspendGetCard(canId)
      	status = card.status
      }
      
      //指定到IO线程,
      suspend fun suspendGetUser(canId: Int) {
      	withContext(Dispatchers.IO) {
      		api.getCard(canId)
      	}
      }
      
      
    • 然后在所有任务都完成以后,协程将自动切回主线程,让剩下的代码回到主线程去执行。这也是为什么用Dispatcher调度器来指定线程,它不仅需要切到指定线程执行任务,还要再切回来。

    so: 挂起就是 暂时切走,一会儿再切(resume)回来。(稍后会被自动切resume回来的线程切换

    Dispatcher调度

    • Dispatcher.Main : 安卓的主线程, 通常UI渲染在这里
    • [Dispatcher.IO](http://dispatcher.IO) :IO工作, 读写文件,网络请求,数据库操作
    • Dispatcher.Default :cpu工作: 计算

    那切回来的意思就是协程会再post一个任务Runnable,让主线程继续执行剩余的代码

  3. suspend函数只能在协程里或者另一个挂起函数里启动

image.png

  • 首先,挂起函数一会儿是需要切回来的,而恢复/切回来/resume这个功能是协程的,所以如果不在协程中启动,就恢复不了。
  • 然后一个挂起函数如果在一个协程/挂起函数中启动,它终归是在一个协程里面被调用的,为的就是让协程可以在挂起函数切换线程后再切回来。
  1. 如何挂起?

image.png 如果是👆这样的, 最终代码还是运行在Main线程里的,因为它不知道往哪儿切。(且android studio会告诉你suspend是多余的)

fun getCard(canId : String, dataSource: DataSource) {
    CoroutineScope(Dispatchers.Main).launch{
        suspendGetCard(canId,dataSource)
    }
}

private suspend fun suspendGetCard(canId : String, dataSource: DataSource) {
    withContext(Dispatchers.IO) {
                    dataSource.getCard(canId)
    }
}

withContext 就是一个挂起函数,然后Dispatcher参数让withContext知道往哪儿切,然后协程才被挂起了,所以实际协程是在withContext才被挂起的,函数前面的 suspend 并没有让协程挂起或者切线程。是suspend函数执行到内部自带的挂起函数的时候挂起的。

  1. suspend关键字作用:提醒
  • 函数的创建者对函数调用者的提醒: 我是一个耗时的任务,因此我被我的创建者用挂起的方式放在了后台运行, 所以请在协程里调用我。
  • 这个提醒就是为了让主线程不卡 ,调用者不用去做这个耗时的切线程的工作,而是创建者去做,调用者只需要接受一个提醒。
  • 这种提醒就形成了一种机制,即所有耗时任务都自动放到后台执行的机制,所有主线程就不卡了。

总结: suspend关键字只是一个提醒,实际进行挂起操作的是挂起函数里面的实际代码。

  1. 怎么自定义suspend函数
  • 什么时候自定义?→耗时的
    • I/O操作 或者 计算工作:

      文件读写,网络交互,图片的模糊,美化处理

    • 本身并不慢,但是比如需要先等待几秒再执行

  • 怎么写?withContext 最常用
suspend fun a(){
    withContext(){}
}

// delay()

非阻塞式挂起

非阻塞是:不卡线程

线程阻塞:

  • 前面有障碍,过不去,线程卡了
  • 等待清除障碍,耗时任务结束
  • 或者绕道,切换线程

协程中的挂起就是指的非阻塞式的挂起,非阻塞就是挂起的一个特点。且阻塞非阻塞都是基于单线程而言的,多线程就不存在阻塞问题了。因此,非阻塞式挂起就是说挂起的同时切换线程。