写在前面
有一个使用kotlin
多线程的爬虫项目,想让爬虫效率更高,于是采用了线程池的方式进行并发,在一台2核8G
的服务器上运行5
分钟就会耗尽cpu
资源,在不限制并发数的情况下调整线程池不能解决这个问题,于是想到了kotlin协程
。
一、如何确定项目中是否需要多线程或协程
可以从以下方向评估:
- 不实时性:协程不像线程一样运行过程是实时的,协程会受自身调度机制影响运行和挂起,这个行为不完全受控(这个行为在 kotlin-协程上下文与调度器 中有详细介绍)。所以不适合用协程做实时性比较高的事情。
- 异步消息:协程内部的数据可以通过基础通道来通讯,但是这本身不是一个字面意义上的通道,有点像复杂度稍高的消息订阅发布模式或观察者模式(双向异步读写),它是一个先进先出的栈,有别于
java atomicreference
,同时对通道的使用也有一些异步限制,类似于nodejs Promise
的概念 - 代码改造:如果代码原来是多线程,那么改成协程几乎是
零成本
,只需要写一个CoroutinesSupport
来创建协程,替换掉原来创建线程的代码即可
二、协程的运作
协程的运作大致可以分为以下几个步骤:
- 初始化协程调度器,根据当前硬件
cpu
情况计算调度器核心工作线程数 - 创建协程运行范围(全局、当前方法范围可挂起、可阻塞等)
- 创建协程(在一个运行范围内可以创建多个协程)并注册到调度器上
- 调度器分配工作线程,创建协程运行资源,绑定到分配的线程上
- 调度器调度协程挂起,等待运行(这个过程是重复的)
三、建议使用协程的方式
通过 kotlin-协程基础 可以很清楚的了解协程的使用方式。但是经过实际的项目使用,建议使用以下方式:
fun exec(runnable: Runnable) {
# 创建一个全局协程
GlobalScope.launch {
# 设置协程作用域-可挂起
coroutineScope {
# 创建协程
launch {
try {
# 协程内部实际运行的代码
runnable.run()
} catch (e: Exception) {
logger.error("协程运行异常", e)
}
}
}
}
}
四、使用协程的注意事项
-
使用
runBlocking
时千万注意,它会阻塞当前调用它的主线程,直到内部的协程全部执行完毕。它相当于thread.join()
- 假设你在
springboot controller
的方法中调用了一个带有runBlocking
的方法,会导致整个springboot
程序卡住,等待协程全部执行完毕才会释放
- 假设你在
-
使用通道时,注意方法头部的
suspend
修饰,这与nodejs Promise
高度相似,会导致调用堆栈的所有上层方法都需要使用suspend
修饰。 -
协程的原子性是无法使用
@Volatile
或者多线程锁来控制的,需要使用newSingleThreadContext
来显式表达上下文,具体参见 kotlin协程-共享的可变状态与并发
五、总结
-
协程是可以提升对结果要求实时性不高的代码执行效率
-
需要认识到协程的工作机制,才能正确使用协程,不要用多线程的知识去理解协程,容易陷入误区
-
kotlin 协程
与golang 协程
有非常多的相似之处,两者可以相互印证便于理解,但api
不是通用的,golang 协程
表现的更加原始和狂野一些。 -
在没有多线程上下文数据同步的情况下,多线程切换到协程上代价是非常低的
评论区