一、协程是什么?
1.1 基本概念的理解
我们知道JVM中的线程的实现是依赖其运行的操作系统决定的,JVM只是在上层进行了API的封装,包含常见的有线程的启动方法,状态的管理,比如:Java中抽象出了6种状态,提供了start方法用于启动线程。
但是线程一旦调用start()开始执行,那我们是很难再控制线程的停止的,尽管jdk中提供了suspend()方法,但是suspend也只是做了标记线程需要中断,最终是否中断,什么时候中断还是依赖操作系统的具体实现逻辑,从语言层面来说是无法直接控制的。
// java线程的状态定义在Java$State枚举对象中
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code Thread.join()}
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
而协程内的代码依然执行在线程上,因为线程是CPU调度的基本单元这个大前提还是不变的,属于操作系统层面的基本概念了。但是协程通过使用状态机的方式在语言层面上实现了一种状态、生命周期更易管控的代码逻辑调度框架(语言层面的框架),也可以理解为轻量级线程,并且不像线程那样直接使用操作系统实现的线程,一旦启动基本就只能等任务执行结束或请求中断待能中断时才停止运行。
1.2 协程和线程、进程的关系
启动一个线程执行任务:
val task1 = Thread {
val result = requestUserInfo()
println("task1 finished, result = $result")
}
task1.start()
启动一个协程执行任务:
val task1 = launch {
val result = requestUserInfo()
println("task1 finished, result = $result")
}
// requestUserInfo()需要切换协程运行的线程需要增加suspend修饰,
// 定义成挂起函数
suspend fun requestUserInfo(): UserInfo = withContext(Dispatchers.IO) {
delay(500)
return@withContext UserInfo("10000", "zhangsan")
}
总结一下,协程和线程的区别:
- 线程一旦开始执行就不会暂停,直到任务结束,这个过程是连续的
- 协程能够自己挂起和恢复,语言层面实现了挂起和恢复流程,能够实现协作式调度
1.3 使用协程的关键API
1.3.1 协程作用域:CoroutineScope
创建协程或调用挂起函数必须有协程作用域,kotlin创建作用域有三种办法,GlobalScope、runBlocking和CoroutineScope()方法。
- Android中提供的协程作用域有:
-
MainScope()
-
lifecycleScope
lifecycleScope中的协程会在Activity销毁时执行cancel -
viewModelScope
-
1.3.2 协程对象:Job
public interface Job : CoroutineContext.Element {
// 注(1)
public companion object Key : CoroutineContext.Key<Job>
// 如果协程还未启动,比如传入的start对象是LAZY,可通过主动调用
// start方法启动协程
public fun start(): Boolean
// 注(2)
public fun cancel(cause: CancellationException? = null)
// 当前协程的子协程
public val children: Sequence<Job>
// 附加子协程,使当前协程对象成为父协程
@InternalCoroutinesApi
public fun attachChild(child: ChildJob): ChildHandle
// 等待当前协程执行完成,比如调用协程的cancel()方法后,调用join()
// 就是等待协程cancel执行完成
public suspend fun join()
// 注册在取消或完成此作业时 同步 调用一次的处理程序
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
@InternalCoroutinesApi
public fun invokeOnCompletion(
onCancelling: Boolean = false,
invokeImmediately: Boolean = true,
handler: CompletionHandler): DisposableHandle
}
- (1)Key : 声明成伴生对象后,只要是同一种类型的Job创建出来的不同Job实例,key都是相同的,也就是同类型的Job对象key也相同
- (2)cancel(): 取消协程
1.3.3 协程上下文:CoroutineContext
存放协程相关的一些信息
1.3.4 协程调度器:CoroutineDispatcher
- Dispatchers.Main: Android中特有的,在主线程中执行协程代码,其他平台使用会抛出异常
- Dispatchers.IO: 用于IO密集型的协程任务
- Dispatchers.Default: 用于CPU密集型的协程任务
- Dispathcers.Unconfined: 不指定协程执行的线程调度器
二、协程在Android中的常见用法
2.1 子线程中执行耗时任务后切到主线程更新UI
// 场景1: 在子线程中执行耗时任务后,切到主线程处理
coroutineScope.launch {
// 挂起函数,执行时从当前线程中脱离,执行在dispatcher执行的线程中,执行完毕后再切换原来的线程中
// 挂起后当前协程下一行代码会等待挂起函数执行完成
val result = withContext(Dispatchers.IO) {
// 在Dispatchers.IO(线程调度器)指定的子线程中执行下面的代码
delay(5000)
100
}
Log.d(TAG, "onCreate: main 2 =========> $coroutineContext")
binding.tvNews.text = result.toString()
}
上面的用法对于Android来说,协程是一个异步代码执行框架,相比于Thread+Handler的方式更加简洁,省去了开发者编写线程切换代码的工作。
2.2 多个耗时任务并行执行合并结果【常见的业务模型】
在Android业务中我们经常需要并行开始多个业务接口请求,然后合并成一个结果,进行后续业务逻辑的判断、UI的展示,使用Jdk提供的CountDownLatch,RxJava的zip都可以实现类似的功能逻辑。
如下展示了kotlin在这个业务模型中如何实现:
coroutineScope.launch {
// 在Dispatchers.IO执行的线程中执行任务1
val async1Result = async(Dispatchers.IO) {
Log.d(TAG, "onCreate: async1 $coroutineContext")
executeTask1()
}
// 在Dispatchers.IO执行的线程中执行任务2
val async2Result = async(Dispatchers.IO) {
Log.d(TAG, "onCreate: async2 $coroutineContext")
executeTask2()
}
// 在调用async方法之后两个协程任务都已经并行跑起来了,这时候调用await方法等待执行结果
val result = async1Result.await() + async2Result.await()
Log.d(TAG, "onCreate: async result = $result")
}
- kotlin中使用async实现类似java中Callable的协程任务,但是await方法阻塞等待结果并没有提供超时时间的参数
- async()方法是创建一个可获取返回值的协程对象,类型是Deferred,继承自Job
三、挂起函数的理解
3.1 挂起函数的本质
- 协程的核心是函数或一段程序能够支持挂起,执行完成后又从挂起位置恢复,然后继续执行后面的代码。
- kotlin的是借助线程实现的,是对线程的一个封装框架,通过launch、async启动一个协程,其实就是启动一个闭包中的代码块。
- 当执行到suspend函数时,暂时不执行协程代码了,而是从当前线程中脱离,函数内的逻辑转到协程调度器所指定的线程中去执行,等到挂起函数执行完毕后,又恢复都挂起的位置,继续执行后续逻辑
所以总结来说,挂起函数就是切到别的线程,稍后又能够自动切回来的线程调度操作。
3.2 为什么挂起函数一定要在协程或挂起函数中调用?
挂起函数切到调度器线程中后,是需要协程框架主动调用resumeWith方法再切回来的,如果在非协程非挂起函数调用,那么就没有协程环境,无法切回来,就无法实现挂起的执行逻辑。
四、协程的取消
协程提供了cancel方法进行取消。
class Job {
public fun cancel(cause: CancellationException? = null)
}
cancel()方法其实还有另外两个重载方法,但是打上了@Deprecated注解,所以不再使用了。
五、协程并发数据同步问题
在java中多线程访问共享变量时,因为有主内存和工作内存模型的设计。
class Test {
private var count = 0
private val mutex = Mutex()
suspend fun test() = withContext(Dispatchers.IO) {
repeat(100) {
launch {
repeat(1000) {
count++
}
}
}
launch {
delay(3000)
println("finished----> count = $count")
}
}
}
fun main(): Unit = runBlocking {
runBlocking {
Test().test()
}
}
输出结果:
finished----> count = 97861
如上代码,在循环中创建100个协程,每个协程中对count变量累加1000次,最终count的值应该是100000,但是输出是97861,存在并发修改数据不一致问题。
在Java线程中,我们可以采用sychronized或ReentrantLock方式进程多线程同步处理,保证最终数据的正确性。