『彻底弄懂kotlin协程系列』–什么是kotlin协程?

前言

kotlin是一门比较新的语言了,自动被谷歌设为android开发的官方语言后,他的趋势大好,使用的人也越来越多。在kotlin中,有很多新的特性,有好用的,也有不太习惯的,但,协程绝对是让我们又爱又恨的。会用的程序员说他很好用,但是入门的学习门槛又好像挺高的,一不小心还绊倒了。

我使用了kotlin一阵子之后,对协程也有了一定的了解,写下这一系列的文章来介绍一下协程,当是分享也是给我自己加深印象。我不是业界仅仅只是一个爱编程的程序猿,有不同的见解,还请评论区一起交流或者私信。

因为kotlin我目前是应用在android开发,所以这一系列的文章也是以android开发为主。。那话不多说,直接开始把

协程是什么?

要了解kotlin的协程,可以先来看一下广义中协程的特点是怎么样的。线程和进程我们都很清楚是什么,协程在网上的很多资料也讲了很多。用户态、非阻塞、高性能等等。但是,协程到底是什么?协程是一种对于并发的处理方案。这里要区分一下并行和并发。

  • 并行:两个任务同时执行,多线程就是这个道理。
  • 并发:多端同时请求同个对象。例如消费者和生产者模型,同时都要拿到那个容器来进行操作

生产者消费者模型:这个模型是非常经典的。简单点来说就是:现在有一个篮子,A往里面放苹果,B从篮子里取苹果吃,当篮子满了,A会等着,篮子空了,B会等着。这样就是这个模型的大致。但是有几个问题

  • 如果A和B同时对篮子进行操作,那么篮子里面的苹果会处于不正常的状态。例如当B拿到篮子的副本取出一个苹果,同时A也拿到篮子,存了一个进去,B吃完更新了篮子的状态,然后A再更新状态回去,这个时候篮子中被B 吃掉的苹果就会回来了。出现了错误。
  • 如果对篮子进行加锁,那么假如篮子是空的,B拿到篮子后,就会等着,而A因为拿不到锁,就会一直等着锁,造成了死锁问题

所以从上面的例子可以看到并发产生的问题,是非常多的,上面只是冰山一角。那协程如何处理这个问题呢?

协程,顾明思义,协作线程,协程是在线程下粒度更小的程序。他可以在程序运行到某个时刻,先挂起,执行别的程序,然后等到合适的情况再回来继续执行。例如上面的生产者消费者,因为生产和消费处于不同的线程,所以,会导致并发问题。而协程处于单线程,所以没有了并发,那单线程如何并行执行任务呢?

我们可以先运行B ,去拿到篮子进行取苹果,然后取一半停下来,先去A 那里生产苹果,A生产了,再回来继续执行B的取苹果,吃完苹果取下一个的时候停下来,再重复同样的操作,通过不断地跳跃代码执行的顺序,实现了代码的“并行”,但不是真正的并行,因为是单线程的,所以不存在并行,只是看起来是“并行的”。

举个栗子,下面是伪代码:

coroutine1:
while(true){
apple = yield //1
eat(apple)
}

coroutine2:
while(true){
apple.send(new apple) //2
product
}

上面的代码是运行在同个线程的,同运行到1的时候,这个代码所在的协程被挂起,然后执行下面的协程,然后到了2的时候,就返回到协程1,吃完苹果循环又回到了协程2继续执行代码,就这样不断在两个协程之间切换代码执行顺序。实现了看似并并的情况。

所以我们可以看到协程他是:

  • 单线程的
  • 通过控制代码的执行顺序实现“并行”但不是真正的并行
  • 解决了并发问题,因为单线程不会出现并发
  • 比线程性能更高,因为他只是通过程序控制了代码的执行顺序,并没有通过操作系统去切换线程,也就是他是“用户态”的

但是:

  • 当协程被阻塞的时候,整个线程都会被阻塞
  • 协程不能处理耗时任务,不然会阻塞线程

到此我们对协程是什么已经有了一个大概的认识。笔者对于协程的认识只是冰山一角,上述解释有误,还请指出。

那kotlin中的协程又是什么?

我们前面讲过,kotlin是基于jvm’的语言,但jvm我们知道,java是不支持协程的啊,也就是.class文件是没有协程的支持的,而kotlin最终是要编译成.class,那kotlin自然是不支持协程,可是kotlin协程,为什么还存在呢?他到底是什么?

对,kotlin是不支持我上面讲的广义中的协程的本质的,因为jvm是不支持的。但是kotlin的协程,本质上是一种线程框架。kotlin通过自动切换线程,来实现协程的优良特性。

举个栗子,(kotlin代码,不要纠结具体语法细节)

//这是一个线程,执行两个方法
GlobalScope.launch{
    while(true){
        getApples() 
        eatApples()
    }
}

//获取苹果的方法
suspend fun getApples(){
    //切换到IO线程
    withContext(IO线程){
        Thread.sleep(3000)
    }    
}
//吃苹果的方法
fun eatApples(){
}

上面我在kotlin的一个协程内实现了一个循环:先取苹果,再吃苹果。

我们会发现我们是直接线性写下来的,按照我们的常规思路,这两个方法都不能是耗时的,如果是耗时的那么需要进行别的线程运行逻辑然后回调对吧,但是我们发现,获取苹果的方法是耗时(Thread.sleep(3000))的,那这样写不就会阻塞线程吗?这才是kotlin协程的巧妙之处。

前面我讲过了,kotlin协程是一个线程框架,他会自动切换线程。我们看到获取苹果的方法里面,我们手动切换到了IO线程执行耗时逻辑,但是居然不用回调吃苹果的方法,这样不就会让吃苹果的方法先执行了吗?不会的,kotlin协程,会在获取苹果的那个地方先挂起,然后等到获取完苹果,再回到协程所在的线程,注意这里吃苹果方法的线程是切回来了,继续执行吃苹果方法,而且不会阻塞线程,因为是在别的线程执行耗时逻辑。

通过上面的一个例子,我们应该可以大概理解了,原来kotlin的协程就是,会自动跳转线程,然后完事还会回到现在的线程,消灭了回调,我们可以用非阻塞的写法来实现阻塞逻辑。是的,如果能了解到这个点,那么我的文章也是有所作用了。

再了解kotlin协程

我们可以发现kotlin的协程和广义的协程在使用上是很像的,都是实现了代码的挂起和恢复。只是他们的本质是不同的。kotin的协程把代码挂起不阻塞线程,然后跑去别的线程执行逻辑,然后搞定了再回来继续执行剩下的逻辑,当然,剩下的逻辑也可以进行挂起。而广义的协程是在单线程内进行代码挂起,可以随意调整代码的执行顺序,而kotlin协程,是没有办法做到这一点的,在jvm上无法随意调整代码的执行顺序。

我们可能有疑问:这样切换线程,是不是开销很大?答案是肯定的,但也不是非常大。这里涉及到源码设计问题,我也不是很理解我就简单说一下我所认识的内容。kotlin内部是维护一个线程池来实现线程切换的,所以他的成本会低了很多。而在线程切换上,底层还是使用Handle来进行切换线程。我前面讲到,kotlin协程他是线程框架,所以他也只是把线程池和handle封装了起来。

那是不是协程就没啥卵用了?不不不,一个框架最重要的是什么?方便!kotlin协程的方便,超乎你想像。这一个文章就不介绍太多已经两千多字了。

小结

这篇文章我们了解了两个点:什么是广义的协程,什么是kotlin的协程。广义的协程是指在代码中调整代码的执行顺序,在单线程解决并发问题,但是不能解决阻塞问题。kotlin的协程由于jvm的限制,他封装线程来实现代码的挂起,用非阻塞的写法来实现阻塞逻辑,但是由于是用到切换线程,所以性能上会比较差。

如果通过这篇文章你对协程有了新的认识,那么我就满足了。另外,协程他到底有多方便?协程要怎么用?协程他怎么指定线程?协程他怎么知道哪个方法是需要挂起的?上面你写的suspend关键字到底是啥?(没看见suspend,回去看一下例子代码)等等。我都会在我的系列文章更新。欢迎交流。

文章到此就结束了,有帮助还请点个赞,评个论,收个藏,转个发,谢谢。